diff --git a/backend/auction/urls.py b/backend/auction/urls.py
index 4094b45..7ea380c 100644
--- a/backend/auction/urls.py
+++ b/backend/auction/urls.py
@@ -4,8 +4,8 @@
from .views import (
AuctionDayApiView,
AuctionDetailApiView,
+ AuctionItemsApiView,
AuctionListApiView,
- AuctionVehiclesApiView,
BidderVerificationApiView,
CurrentAuctionApiView,
GetSavedUnitApiView,
@@ -29,7 +29,7 @@
),
path(
"//days//vehicles",
- AuctionVehiclesApiView.as_view(),
+ AuctionItemsApiView.as_view(),
name="auction_vehicles",
),
path(
diff --git a/backend/auction/views.py b/backend/auction/views.py
index d7cd469..a5ab3f5 100644
--- a/backend/auction/views.py
+++ b/backend/auction/views.py
@@ -1,10 +1,12 @@
from datetime import timedelta
from django.contrib.contenttypes.models import ContentType
+from django.core.paginator import Paginator
from django.db.models import Prefetch, Q
from django.utils import timezone
from rest_framework import status
from rest_framework.generics import get_object_or_404
+from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
@@ -245,8 +247,6 @@ def post(self, request, **kwargs):
vehicle_id = kwargs.get("vehicle_id")
auction_id = kwargs.get("auction_id")
- # Replace this later to fetch authentication details
- # from headers instead of body
vehicle = get_object_or_404(Vehicle, id=vehicle_id)
auction_for_vehicle = Auction.objects.get(id=auction_id)
bidder = self.cognitoService.get_user_details(bidder_id)
@@ -316,48 +316,79 @@ def get(self, request, **kwargs):
return Response({"vehicles": vehicle_data}, status=status.HTTP_200_OK)
-class AuctionVehiclesApiView(APIView):
- """
- An endpoint to retrieve an auction's associated vehicles
- """
+class AuctionItemsApiView(APIView):
+ permission_classes = [IsAuthenticated]
- cognitoService = AWSCognitoService()
+ def get_filtered_queryset(self, model, auction_day_id, filters):
+ """
+ Filters the queryset of a given model based on the provided filters
+ and the relation to AuctionItem through ContentType.
+ """
+ content_type = ContentType.objects.get_for_model(model)
+ related_item_ids = AuctionItem.objects.filter(
+ auction_day_id=auction_day_id, content_type=content_type
+ ).values_list("object_id", flat=True)
- def get_permissions(self):
- if self.request.method == "GET":
- self.permission_classes = [IsAuthenticated]
- else:
- self.permission_classes = [IsAdminUser]
- return super().get_permissions()
+ return model.objects.filter(id__in=related_item_ids, **filters)
def get(self, request, auction_id, auction_day_id):
- auction_day = get_object_or_404(AuctionDay, id=auction_day_id)
-
- auction_items = AuctionItem.objects.filter(auction_day=auction_day)
+ item_type = request.query_params.get("item_type", None)
+ type = request.query_params.get("type", None)
+ brand_id = request.query_params.get("brand", None)
+ min_price = request.query_params.get("min_price", None)
+ max_price = request.query_params.get("max_price", None)
+ search_term = request.query_params.get("search", None)
+
+ filters = {}
+ if type:
+ filters["type"] = type
+ if brand_id:
+ filters["brand"] = brand_id
+ if min_price:
+ filters["current_price__gte"] = min_price
+ if max_price:
+ filters["current_price__lte"] = max_price
+ if search_term:
+ filters["description__icontains"] = search_term
+
+ # Apply filters to each model's queryset
+ if item_type == "trucks":
+ combined_qs = self.get_filtered_queryset(Vehicle, auction_day_id, filters)
+ elif item_type == "equipment":
+ combined_qs = self.get_filtered_queryset(Equipment, auction_day_id, filters)
+ elif item_type == "trailers":
+ combined_qs = self.get_filtered_queryset(Trailer, auction_day_id, filters)
+ else:
+ vehicle_qs = self.get_filtered_queryset(Vehicle, auction_day_id, filters)
+ equipment_qs = self.get_filtered_queryset(
+ Equipment, auction_day_id, filters
+ )
+ trailer_qs = self.get_filtered_queryset(Trailer, auction_day_id, filters)
- vehicle_list = []
- equipment_list = []
- trailer_list = []
+ combined_qs = list(vehicle_qs) + list(equipment_qs) + list(trailer_qs)
- for auction_item in auction_items:
- if isinstance(auction_item.content_object, Vehicle):
- vehicle_list.append(auction_item.content_object)
- elif isinstance(auction_item.content_object, Equipment):
- equipment_list.append(auction_item.content_object)
- elif isinstance(auction_item.content_object, Trailer):
- trailer_list.append(auction_item.content_object)
+ # Implement custom pagination
+ page_number = request.query_params.get("page", 1)
+ paginator = Paginator(combined_qs, 5)
+ page_obj = paginator.get_page(page_number)
- vehicle_data = [{"id": vehicle.id} for vehicle in vehicle_list]
- equipment_data = [{"id": equipment.id} for equipment in equipment_list]
- trailer_data = [{"id": trailer.id} for trailer in trailer_list]
+ serialized_data = []
+ for obj in page_obj:
+ if isinstance(obj, Vehicle):
+ serializer = VehicleSerializer(obj)
+ elif isinstance(obj, Equipment):
+ serializer = EquipmentSerializer(obj)
+ elif isinstance(obj, Trailer):
+ serializer = TrailerSerializer(obj)
+ serialized_data.append(serializer.data)
return Response(
{
- "vehicles": vehicle_data,
- "equipment": equipment_data,
- "trailers": trailer_data,
- },
- status=status.HTTP_200_OK,
+ "count": paginator.count,
+ "next": page_obj.has_next(),
+ "previous": page_obj.has_previous(),
+ "results": serialized_data,
+ }
)
def post(self, request, auction_id, auction_day_id):
diff --git a/backend/bid/admin.py b/backend/bid/admin.py
index b74914c..b688e4f 100644
--- a/backend/bid/admin.py
+++ b/backend/bid/admin.py
@@ -10,12 +10,13 @@ class BidAdmin(admin.ModelAdmin):
"amount",
"bidder",
"auction",
+ "auction_day",
"content_type",
"object_id",
"created_at",
)
- search_fields = ("bidder__username", "auction__name", "amount")
- list_filter = ("auction", "bidder", "content_type")
+ search_fields = ("bidder", "amount")
+ list_filter = ("auction", "auction_day", "content_type")
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "content_type":
diff --git a/backend/bid/migrations/0003_bid_auction_day.py b/backend/bid/migrations/0003_bid_auction_day.py
new file mode 100644
index 0000000..73e595c
--- /dev/null
+++ b/backend/bid/migrations/0003_bid_auction_day.py
@@ -0,0 +1,24 @@
+# Generated by Django 5.0.4 on 2024-04-10 19:30
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("auction", "0003_alter_auctionverifieduser_cognito_user_id"),
+ ("bid", "0002_alter_bid_bidder"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="bid",
+ name="auction_day",
+ field=models.ForeignKey(
+ default=0,
+ on_delete=django.db.models.deletion.PROTECT,
+ to="auction.auctionday",
+ ),
+ preserve_default=False,
+ ),
+ ]
diff --git a/backend/bid/models.py b/backend/bid/models.py
index 363e0a2..325df28 100644
--- a/backend/bid/models.py
+++ b/backend/bid/models.py
@@ -2,7 +2,7 @@
from django.contrib.contenttypes.models import ContentType
from django.db import models
-from auction.models import Auction
+from auction.models import Auction, AuctionDay
from core.models import MainModel
@@ -14,6 +14,7 @@ class Bid(MainModel):
amount = models.IntegerField(null=False)
bidder = models.CharField(null=False, max_length=500)
auction = models.ForeignKey(Auction, on_delete=models.PROTECT)
+ auction_day = models.ForeignKey(AuctionDay, on_delete=models.PROTECT)
content_type = models.ForeignKey(ContentType, on_delete=models.PROTECT)
object_id = models.UUIDField()
content_object = GenericForeignKey("content_type", "object_id")
diff --git a/backend/bid/signals.py b/backend/bid/signals.py
index a825361..d4b6382 100644
--- a/backend/bid/signals.py
+++ b/backend/bid/signals.py
@@ -1,8 +1,7 @@
# signals.py
-import json
-
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
+from django.core.exceptions import ObjectDoesNotExist
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
@@ -13,24 +12,42 @@
channel_layer = get_channel_layer()
+def update_current_price(bid_instance):
+ content_type = bid_instance.content_type
+ object_id = bid_instance.object_id
+ bid_amount = bid_instance.amount
+
+ model_class = content_type.model_class()
+
+ try:
+ if hasattr(model_class, "current_price"):
+ item = model_class.objects.get(pk=object_id)
+ item.current_price = bid_amount
+ item.save()
+ except ObjectDoesNotExist:
+ print(f"Item of type {content_type.model} with ID {object_id} not found.")
+
+
@receiver(post_save, sender=Bid)
-def bid_updated(sender, instance, **kwargs):
- current_date = instance.auction.start_date.date()
- auction_day = AuctionDay.objects.filter(
- auction=instance.auction, date=current_date
- ).first()
-
- bid_data = {
- "id": str(instance.id),
- "amount": instance.amount,
- "auction_id": str(instance.auction.id),
- "auction_day_id": str(auction_day.id) if auction_day else None,
- "vehicle_id": str(instance.object_id),
- "bidder": str(instance.bidder),
- }
- async_to_sync(channel_layer.group_send)(
- "bid_updates", {"type": "bid.update", "bid_data": bid_data}
- )
+def bid_updated(sender, instance, created, **kwargs):
+ if created:
+ bid_data = {
+ "id": str(instance.id),
+ "amount": instance.amount,
+ "auction_id": str(instance.auction.id),
+ "auction_day_id": str(instance.auction_day.id)
+ if instance.auction_day
+ else None,
+ "object_id": str(instance.object_id),
+ "bidder_id": str(instance.bidder),
+ }
+
+ async_to_sync(channel_layer.group_send)(
+ "auction_{}".format(instance.auction_id),
+ {"type": "bid.update", "bid_data": bid_data},
+ )
+
+ update_current_price(instance)
@receiver(post_delete, sender=Bid)
diff --git a/backend/bid/views.py b/backend/bid/views.py
index 4ad0985..a673319 100644
--- a/backend/bid/views.py
+++ b/backend/bid/views.py
@@ -1,11 +1,12 @@
+from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from rest_framework import status
from rest_framework.generics import get_object_or_404
-from rest_framework.permissions import AllowAny, IsAuthenticated
+from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
-from auction.models import Auction
+from auction.models import Auction, AuctionDay
from services.AWSCognitoService import AWSCognitoService
from .models import Bid
@@ -14,91 +15,57 @@
class BidListApiView(APIView):
permission_classes = [IsAuthenticated]
-
serializer_class = BidSerializer
cognitoService = AWSCognitoService()
- def get(self, request, *args, **kwargs):
- """
- Get all bids
- """
- bids = Bid.objects.all()
- serializer = BidSerializer(bids, many=True)
- return Response(serializer.data, status=status.HTTP_200_OK)
-
def post(self, request, *args, **kwargs):
- """
- Create a bid
- """
data = request.data
- amount = data.get("amount")
- bidder_id = data.get("bidder")
- auction_id = data.get("auction")
- content_type_name = data.get("content_type")
- object_id = data.get("object_id")
-
- # Check for missing fields
- if not all([amount, bidder_id, auction_id, content_type_name, object_id]):
- return Response(
- {"error": "Missing fields in request."},
- status=status.HTTP_400_BAD_REQUEST,
- )
+ item_type = data.get("content_type", "truck")
+
+ model_map = {
+ "truck": "vehicle",
+ "equipment": "equipment",
+ "trailer": "trailer",
+ }
+ model_name = model_map.get(item_type, "vehicle")
- # Validate ContentType
try:
- content_type = ContentType.objects.get(model=content_type_name)
- if content_type_name not in ["vehicle", "equipment", "trailer"]:
- raise ValueError("Invalid content type")
- except (ContentType.DoesNotExist, ValueError):
+ model = apps.get_model(app_label="vehicle", model_name=model_name)
+ content_type = ContentType.objects.get_for_model(model)
+ except (LookupError, ContentType.DoesNotExist):
return Response(
{"error": "Invalid content type."}, status=status.HTTP_400_BAD_REQUEST
)
- # Validate bidder and auction
- try:
- auction = Auction.objects.get(id=auction_id)
- except Auction.DoesNotExist:
+ required_fields = ["amount", "bidder_id", "auction_id", "object_id"]
+ if not all(field in data for field in required_fields):
return Response(
- {"error": "Invalid auction."},
+ {"error": "Missing fields in request."},
status=status.HTTP_400_BAD_REQUEST,
)
- bidder = self.cognitoService.get_user_details(bidder_id)
+ bidder = self.cognitoService.get_user_details(data.get("bidder_id"))
if not bidder:
return Response(
- {"error": "Invalid bidder."},
- status=status.HTTP_400_BAD_REQUEST,
+ {"error": "Invalid bidder."}, status=status.HTTP_400_BAD_REQUEST
)
- # Validate object_id
- model_class = content_type.model_class()
- try:
- model_class.objects.get(id=object_id)
- except model_class.DoesNotExist:
- return Response(
- {"error": "Invalid object ID for the given content type."},
- status=status.HTTP_400_BAD_REQUEST,
- )
+ item = get_object_or_404(model, id=data.get("object_id"))
- highest_bid = (
- Bid.objects.filter(
- content_type=content_type, object_id=data.get("object_id")
- )
- .order_by("-amount")
- .first()
- )
- if highest_bid and int(amount) <= highest_bid.amount:
+ highest_bid = Bid.objects.filter(item=item).order_by("-amount").first()
+ if highest_bid and int(data["amount"]) <= highest_bid.amount:
return Response(
{"error": "Your bid must be higher than the current highest bid."},
status=status.HTTP_400_BAD_REQUEST,
)
bid = Bid.objects.create(
- amount=amount,
- bidder=bidder,
- auction=auction,
+ amount=data["amount"],
+ bidder=data["bidder_id"],
+ auction_id=data["auction_id"],
+ auction_day_id=data.get("auction_day_id"),
content_type=content_type,
- object_id=object_id,
+ object_id=data["object_id"],
)
serialized_data = self.serializer_class(bid)
@@ -106,10 +73,6 @@ def post(self, request, *args, **kwargs):
class BidDetailApiView(APIView):
- """
- Retrieve, update or delete an bid instance.
- """
-
permission_classes = [IsAuthenticated]
def get(self, request, bid_id, format=None):
@@ -119,7 +82,7 @@ def get(self, request, bid_id, format=None):
def put(self, request, bid_id, format=None):
bid = get_object_or_404(Bid, id=bid_id)
- serializer = BidSerializer(bid, data=request.data)
+ serializer = BidSerializer(bid, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
diff --git a/backend/vehicle/admin.py b/backend/vehicle/admin.py
index e75e8cf..c5017d6 100644
--- a/backend/vehicle/admin.py
+++ b/backend/vehicle/admin.py
@@ -1,4 +1,5 @@
from django.contrib import admin
+from django.contrib.contenttypes.admin import GenericTabularInline
from django.contrib.contenttypes.models import ContentType
from .models import (
@@ -13,14 +14,21 @@
)
+class UnitImageInline(GenericTabularInline):
+ model = UnitImage
+ extra = 1
+
+
class BrandAdmin(admin.ModelAdmin):
list_display = ("id", "name", "created_at")
search_fields = ("name",)
+ readonly_fields = ("created_at",)
class TypeAdmin(admin.ModelAdmin):
list_display = ("id", "name", "created_at")
search_fields = ("name",)
+ readonly_fields = ("created_at",)
class VehicleAdmin(admin.ModelAdmin):
@@ -31,7 +39,7 @@ class VehicleAdmin(admin.ModelAdmin):
"model_number",
"chassis_number",
"brand",
- "vehicle_type",
+ "type",
"is_sold",
"created_at",
)
@@ -39,60 +47,69 @@ class VehicleAdmin(admin.ModelAdmin):
"model_number",
"chassis_number",
"brand__name",
- "vehicle_type__name",
+ "type__name",
)
- list_filter = ("brand", "vehicle_type", "is_sold")
+ list_filter = ("brand", "type", "is_sold")
+ readonly_fields = ("created_at",)
+ inlines = [UnitImageInline]
class EquipmentAdmin(admin.ModelAdmin):
list_display = (
"id",
"unicode_id",
- "prefix_id",
+ "model_number",
"chassis_number",
"engine_number",
"brand",
- "equipment_type",
+ "type",
"created_at",
)
search_fields = (
- "prefix_id",
+ "model_number",
"chassis_number",
"engine_number",
"brand__name",
- "equipment_type__name",
+ "type__name",
)
- list_filter = ("brand", "equipment_type")
+ list_filter = ("brand", "type")
+ readonly_fields = ("created_at",)
+ inlines = [UnitImageInline]
class SupplierAdmin(admin.ModelAdmin):
list_display = ("id", "name", "created_at")
search_fields = ("name",)
+ readonly_fields = ("created_at",)
class TrailerAdmin(admin.ModelAdmin):
list_display = (
"id",
"unicode_id",
- "chassis_number",
+ "model_number",
"supplier",
- "trailer_type",
+ "type",
"number_of_axles",
"created_at",
)
- search_fields = ("chassis_number", "supplier__name", "trailer_type__name")
- list_filter = ("supplier", "trailer_type")
+ search_fields = ("model_number", "supplier__name", "type__name")
+ list_filter = ("supplier", "type")
+ readonly_fields = ("created_at",)
+ inlines = [UnitImageInline]
class UnitImageAdmin(admin.ModelAdmin):
list_display = ("id", "image_url", "content_type", "object_id", "created_at")
search_fields = ("content_type__model",)
list_filter = ("content_type",)
+ readonly_fields = ("created_at",)
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "content_type":
- valid_models = ["vehicle", "equipment", "trailer"]
- kwargs["queryset"] = ContentType.objects.filter(model__in=valid_models)
+ kwargs["queryset"] = ContentType.objects.filter(
+ model__in=["vehicle", "equipment", "trailer"]
+ )
return super().formfield_for_foreignkey(db_field, request, **kwargs)
@@ -107,15 +124,17 @@ class SavedUnitsAdmin(admin.ModelAdmin):
)
search_fields = ("auction_id__name", "content_type__model")
list_filter = ("auction_id", "content_type")
+ readonly_fields = ("created_at",)
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "content_type":
- valid_models = ["vehicle", "equipment", "trailer"]
- kwargs["queryset"] = ContentType.objects.filter(model__in=valid_models)
+ kwargs["queryset"] = ContentType.objects.filter(
+ model__in=["vehicle", "equipment", "trailer"]
+ )
return super().formfield_for_foreignkey(db_field, request, **kwargs)
-# Register your models here
+# Register your models and their admins
admin.site.register(Brand, BrandAdmin)
admin.site.register(Type, TypeAdmin)
admin.site.register(Vehicle, VehicleAdmin)
diff --git a/backend/vehicle/helpers.py b/backend/vehicle/helpers.py
index 2d1f1ed..37107b4 100644
--- a/backend/vehicle/helpers.py
+++ b/backend/vehicle/helpers.py
@@ -6,46 +6,14 @@
from .serializers import VehicleSerializer
-def infinite_filter(request):
- url_parameter = request.GET.get("search")
- if url_parameter:
- limit = request.GET.get("l")
- offset = request.GET.get("o")
- if limit and offset:
- return Vehicle.objects.filter(unicode_id__icontains=url_parameter)[
- : int(offset) + int(limit)
- ]
- elif limit:
- return Vehicle.objects.filter(unicode_id__icontains=url_parameter)[
- : int(limit)
- ]
- else:
- return Vehicle.objects.filter(unicode_id__icontains=url_parameter)[:15]
- return Vehicle.objects.all()[:40]
-
-
-def has_more_data(request):
- offset = request.GET.get("o")
- limit = request.GET.get("l")
- if offset:
- return Vehicle.objects.all().count() > (int(offset) + int(limit))
-
- elif limit:
- return Vehicle.objects.all().count() > int(limit)
- else:
- return False
-
-
def parse_excel_to_vehicle(excel_file):
df0 = pd.read_excel(excel_file, sheet_name=0, engine="openpyxl")
- # empty list to store Vehicle object
vehicles = []
- # iterate through rows in dataframe
for index, row in df0.iterrows():
if pd.isna(row.iloc[2]) or pd.isna(row.iloc[19]) or pd.isna(row.iloc[1]):
- continue # exit the loop if an empty cell is encountered
+ continue
vehicle_data = {
"unicode_id": row["UNICODE"],
@@ -54,7 +22,7 @@ def parse_excel_to_vehicle(excel_file):
"engine_number": row["ENGINE NUMBER"],
"description": row["DESCRIPTION"],
"brand": row["BRAND"],
- "vehicle_type": row["TYPE"],
+ "type": row["TYPE"],
"minimum_price": row["SALE PRICE (PhP)"],
# "is_sold": row[""],
"remarks": row["REMARKS"],
@@ -71,7 +39,7 @@ def parse_excel_to_vehicle(excel_file):
"body_condition": row["Body"],
}
brand_id = vehicle_data.pop("brand", None)
- type_id = vehicle_data.pop("vehicle_type", None)
+ type_id = vehicle_data.pop("type", None)
brand = Brand.objects.filter(name=brand_id).first()
if brand is None:
brand = Brand.objects.create(name=brand_id)
diff --git a/backend/vehicle/migrations/0002_rename_minimum_price_vehicle_current_price_and_more.py b/backend/vehicle/migrations/0002_rename_minimum_price_vehicle_current_price_and_more.py
new file mode 100644
index 0000000..c472958
--- /dev/null
+++ b/backend/vehicle/migrations/0002_rename_minimum_price_vehicle_current_price_and_more.py
@@ -0,0 +1,75 @@
+# Generated by Django 5.0.4 on 2024-04-10 19:30
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("vehicle", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name="vehicle",
+ old_name="minimum_price",
+ new_name="current_price",
+ ),
+ migrations.RemoveField(
+ model_name="equipment",
+ name="prefix_id",
+ ),
+ migrations.RemoveField(
+ model_name="trailer",
+ name="chassis_number",
+ ),
+ migrations.AddField(
+ model_name="equipment",
+ name="current_price",
+ field=models.IntegerField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name="equipment",
+ name="model_number",
+ field=models.CharField(blank=True, max_length=150, null=True),
+ ),
+ migrations.AddField(
+ model_name="equipment",
+ name="selling_price",
+ field=models.IntegerField(default=0),
+ ),
+ migrations.AddField(
+ model_name="equipment",
+ name="starting_price",
+ field=models.IntegerField(default=0),
+ ),
+ migrations.AddField(
+ model_name="trailer",
+ name="current_price",
+ field=models.IntegerField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name="trailer",
+ name="model_number",
+ field=models.CharField(blank=True, max_length=150, null=True),
+ ),
+ migrations.AddField(
+ model_name="trailer",
+ name="selling_price",
+ field=models.IntegerField(default=0),
+ ),
+ migrations.AddField(
+ model_name="trailer",
+ name="starting_price",
+ field=models.IntegerField(default=0),
+ ),
+ migrations.AlterField(
+ model_name="equipment",
+ name="chassis_number",
+ field=models.CharField(blank=True, max_length=150, null=True),
+ ),
+ migrations.AlterField(
+ model_name="equipment",
+ name="engine_number",
+ field=models.CharField(blank=True, max_length=150, null=True),
+ ),
+ ]
diff --git a/backend/vehicle/migrations/0003_rename_equipment_type_equipment_type_and_more.py b/backend/vehicle/migrations/0003_rename_equipment_type_equipment_type_and_more.py
new file mode 100644
index 0000000..8e80f95
--- /dev/null
+++ b/backend/vehicle/migrations/0003_rename_equipment_type_equipment_type_and_more.py
@@ -0,0 +1,27 @@
+# Generated by Django 5.0.4 on 2024-04-11 00:00
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("vehicle", "0002_rename_minimum_price_vehicle_current_price_and_more"),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name="equipment",
+ old_name="equipment_type",
+ new_name="type",
+ ),
+ migrations.RenameField(
+ model_name="trailer",
+ old_name="trailer_type",
+ new_name="type",
+ ),
+ migrations.RenameField(
+ model_name="vehicle",
+ old_name="vehicle_type",
+ new_name="type",
+ ),
+ ]
diff --git a/backend/vehicle/models.py b/backend/vehicle/models.py
index 7a8acbf..026386c 100644
--- a/backend/vehicle/models.py
+++ b/backend/vehicle/models.py
@@ -21,11 +21,11 @@ class Vehicle(MainModel):
engine_number = models.CharField(max_length=150, blank=True, null=True)
selling_price = models.IntegerField(default=0)
starting_price = models.IntegerField(default=0)
+ current_price = models.IntegerField(blank=True, null=True)
chassis_number = models.CharField(max_length=50, blank=True, null=True)
description = models.CharField(max_length=2000)
brand = models.ForeignKey(Brand, on_delete=models.PROTECT)
- vehicle_type = models.ForeignKey(Type, on_delete=models.PROTECT)
- minimum_price = models.IntegerField(blank=True, null=True)
+ type = models.ForeignKey(Type, on_delete=models.PROTECT)
is_sold = models.BooleanField(default=False)
remarks = models.CharField(max_length=2000, blank=True, null=True)
classification_type = models.CharField(max_length=50, blank=True, null=True)
@@ -41,12 +41,15 @@ class Vehicle(MainModel):
class Equipment(MainModel):
unicode_id = models.IntegerField(unique=True)
- prefix_id = models.CharField(max_length=10)
- chassis_number = models.CharField(max_length=50, blank=True, null=True)
- engine_number = models.CharField(max_length=50, blank=True, null=True)
+ model_number = models.CharField(max_length=150, blank=True, null=True)
+ chassis_number = models.CharField(max_length=150, blank=True, null=True)
+ engine_number = models.CharField(max_length=150, blank=True, null=True)
description = models.CharField(max_length=2000)
brand = models.ForeignKey(Brand, on_delete=models.PROTECT)
- equipment_type = models.ForeignKey(Type, on_delete=models.PROTECT)
+ type = models.ForeignKey(Type, on_delete=models.PROTECT)
+ selling_price = models.IntegerField(default=0)
+ starting_price = models.IntegerField(default=0)
+ current_price = models.IntegerField(blank=True, null=True)
location = models.CharField(max_length=50, blank=True, null=True)
classification_type = models.CharField(max_length=50, blank=True, null=True)
engine_condition = models.CharField(max_length=100, blank=True, null=True)
@@ -70,11 +73,14 @@ class Supplier(MainModel):
class Trailer(MainModel):
unicode_id = models.IntegerField(unique=True)
- chassis_number = models.CharField(max_length=50, blank=True, null=True)
+ model_number = models.CharField(max_length=150, blank=True, null=True)
description = models.CharField(max_length=2000)
supplier = models.ForeignKey(Supplier, on_delete=models.PROTECT)
- trailer_type = models.ForeignKey(Type, on_delete=models.PROTECT)
+ type = models.ForeignKey(Type, on_delete=models.PROTECT)
number_of_axles = models.IntegerField()
+ selling_price = models.IntegerField(default=0)
+ starting_price = models.IntegerField(default=0)
+ current_price = models.IntegerField(blank=True, null=True)
class UnitImage(MainModel):
diff --git a/backend/vehicle/serializers.py b/backend/vehicle/serializers.py
index 0d69c34..fff509b 100644
--- a/backend/vehicle/serializers.py
+++ b/backend/vehicle/serializers.py
@@ -1,67 +1,69 @@
+from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers
from .models import Brand, Equipment, Supplier, Trailer, Type, UnitImage, Vehicle
-class BrandSerializer(serializers.ModelSerializer):
- class Meta:
- model = Brand
- fields = ["id", "name"]
-
-
-class TypeSerializer(serializers.ModelSerializer):
- class Meta:
- model = Type
- fields = ["id", "name"]
-
-
class EquipmentSerializer(serializers.ModelSerializer):
+ brand_name = serializers.SerializerMethodField()
+ type_name = serializers.SerializerMethodField()
+ content_type = serializers.SerializerMethodField()
+
class Meta:
model = Equipment
fields = [
"id",
"unicode_id",
- "prefix_id",
+ "model_number",
"chassis_number",
"engine_number",
"description",
- "brand",
- "equipment_type",
- "location",
- "classification_type",
- "engine_condition",
- "transmission_condition",
- "differentials_condition",
- "brake_condition",
- "electrical_condition",
- "hydraulic_cylindar_condition",
- "hydraulic_hoses_and_chrome_condition",
- "chassis_condition",
- "body_condition",
+ "brand_name",
+ "type_name",
+ "starting_price",
+ "current_price",
+ "content_type",
]
+ def get_brand_name(self, obj):
+ return obj.brand.name
+
+ def get_type_name(self, obj):
+ return obj.type.name
+
+ def get_content_type(self, obj):
+ return obj._meta.model_name
+
class VehicleSerializer(serializers.ModelSerializer):
brand_name = serializers.SerializerMethodField()
- vehicle_type_name = serializers.SerializerMethodField()
+ type_name = serializers.SerializerMethodField()
+ content_type = serializers.SerializerMethodField()
class Meta:
model = Vehicle
fields = [
"id",
+ "unicode_id",
"model_number",
"engine_number",
"chassis_number",
"description",
"brand_name",
- "vehicle_type_name",
+ "type_name",
+ "starting_price",
+ "current_price",
+ "content_type",
]
def get_brand_name(self, obj):
return obj.brand.name
- def get_vehicle_type_name(self, obj):
- return obj.vehicle_type.name
+ def get_type_name(self, obj):
+ return obj.type.name
+
+ def get_content_type(self, obj):
+ return obj._meta.model_name
class SupplierSerializer(serializers.ModelSerializer):
@@ -71,17 +73,32 @@ class Meta:
class TrailerSerializer(serializers.ModelSerializer):
+ brand_name = serializers.SerializerMethodField()
+ type_name = serializers.SerializerMethodField()
+ content_type = serializers.SerializerMethodField()
+
class Meta:
model = Trailer
fields = [
"id," "unicode_id",
- "chassis_number",
+ "model_number",
"description",
- "supplier",
- "trailer_type",
- "number_of_axles",
+ "supplier_name",
+ "type_name",
+ "starting_price",
+ "current_price",
+ "content_type",
]
+ def get_supplier_name(self, obj):
+ return obj.supplier.name
+
+ def get_type_name(self, obj):
+ return obj.type.name
+
+ def get_content_type(self, obj):
+ return obj._meta.model_name
+
class UnitImageSerializer(serializers.ModelSerializer):
unit_type = serializers.SerializerMethodField()
@@ -94,3 +111,15 @@ def get_unit_type(self, obj):
if obj.content_type:
return obj.content_type.model
return None
+
+
+class BrandSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Brand
+ fields = ["id", "name"]
+
+
+class TypeSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Type
+ fields = ["id", "name"]
diff --git a/backend/vehicle/urls.py b/backend/vehicle/urls.py
index 6b9289c..0a6eded 100644
--- a/backend/vehicle/urls.py
+++ b/backend/vehicle/urls.py
@@ -2,21 +2,17 @@
from django.urls import include, path
from .views import (
+ BrandListApiView,
+ DetailApiView,
+ ItemListApiView,
+ TypeListApiView,
UploadFileView,
- VehicleDetailApiView,
- VehicleFilterList,
- VehicleListApiView,
- VehiclePriceApiView,
)
urlpatterns = [
- path("", VehicleListApiView.as_view(), name="vehicle"),
- path("/filter", VehicleFilterList.as_view(), name="vehicle-list"),
- path("/", VehicleDetailApiView.as_view(), name="vehicle_detail"),
- path(
- "//minimum_price",
- VehiclePriceApiView.as_view(),
- name="vehicle_price",
- ),
+ path("", ItemListApiView.as_view(), name="vehicle"),
+ path("/", DetailApiView.as_view(), name="vehicle_detail"),
path("/upload-file", UploadFileView.as_view(), name="upload_file"),
+ path("/brands", BrandListApiView.as_view(), name="brands"),
+ path("/types", TypeListApiView.as_view(), name="types"),
]
diff --git a/backend/vehicle/views.py b/backend/vehicle/views.py
index f8a39d3..296e209 100644
--- a/backend/vehicle/views.py
+++ b/backend/vehicle/views.py
@@ -1,6 +1,8 @@
import pandas as pd
+from django.db.models import Q
from rest_framework import permissions, status
from rest_framework.generics import get_object_or_404
+from rest_framework.pagination import PageNumberPagination
from rest_framework.parsers import FileUploadParser
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
@@ -8,15 +10,25 @@
from core.permissions import IsAdminUser
-from .helpers import has_more_data, infinite_filter, parse_excel_to_vehicle
-from .models import Brand, Type, Vehicle
-from .serializers import VehicleSerializer
+from .helpers import parse_excel_to_vehicle
+from .models import Brand, Equipment, Trailer, Type, Vehicle
+from .serializers import (
+ BrandSerializer,
+ EquipmentSerializer,
+ TrailerSerializer,
+ TypeSerializer,
+ VehicleSerializer,
+)
-# Create your views here.
-class VehicleListApiView(APIView):
- serializer_class = VehicleSerializer
+class StandardResultsPagination(PageNumberPagination):
+ page_size = 5
+ page_size_query_param = "page_size"
+ max_page_size = 100
+
+# Create your views here.
+class ItemListApiView(APIView):
def get_permissions(self):
if self.request.method == "POST":
self.permission_classes = [IsAdminUser]
@@ -26,11 +38,58 @@ def get_permissions(self):
def get(self, request, *args, **kwargs):
"""
- Get all vehicles
+ Get a list of items filtered by type, brand, current price range,
+ and search term. Supports pagination.
"""
- vehicles = Vehicle.objects.all()
- serializer = VehicleSerializer(vehicles, many=True)
- return Response(serializer.data, status=status.HTTP_200_OK)
+ item_type = request.query_params.get("type", None)
+ brand_id = request.query_params.get("brand", None)
+ min_price = request.query_params.get("min_price", None)
+ max_price = request.query_params.get("max_price", None)
+ search_term = request.query_params.get("search", None)
+ paginator = StandardResultsPagination()
+
+ filters = Q()
+ if brand_id:
+ filters &= Q(brand_id=brand_id)
+ if min_price:
+ filters &= Q(current_price__gte=min_price)
+ if max_price:
+ filters &= Q(current_price__lte=max_price)
+ if search_term:
+ filters &= Q(description__icontains=search_term)
+
+ if item_type is None:
+ items_query = (
+ Vehicle.objects.filter(filters)
+ | Trailer.objects.filter(filters)
+ | Equipment.objects.filter(filters)
+ ).distinct()
+ else:
+ type_map = {
+ "truck": Vehicle,
+ "equipment": Equipment,
+ "trailer": Trailer,
+ }
+ model_cls = type_map.get(item_type, Vehicle)
+ items_query = model_cls.objects.filter(filters)
+
+ page = paginator.paginate_queryset(items_query, request)
+ if page is not None:
+ serializer_cls = (
+ VehicleSerializer
+ if item_type == "truck"
+ else TrailerSerializer
+ if item_type == "trailer"
+ else EquipmentSerializer
+ )
+ serialized_page = serializer_cls(
+ page, many=True, context={"request": request}
+ )
+ return paginator.get_paginated_response(serialized_page.data)
+
+ return Response(
+ {"detail": "Invalid request"}, status=status.HTTP_400_BAD_REQUEST
+ )
def post(self, request, *args, **kwargs):
"""
@@ -39,118 +98,65 @@ def post(self, request, *args, **kwargs):
data = request.data.copy()
brand_id = data.pop("brand", None)
type_id = data.pop("type", None)
- # Handling the possibility that brand or type IDs might not exist
+
brand = get_object_or_404(Brand, id=brand_id) if brand_id else None
vehicle_type = get_object_or_404(Type, id=type_id) if type_id else None
- vehicle = Vehicle.objects.create(brand=brand, vehicle_type=vehicle_type, **data)
- # Use the serializer class's data directly
+ vehicle = Vehicle.objects.create(brand=brand, type=vehicle_type, **data)
+
serialized_data = self.serializer_class(vehicle)
return Response(serialized_data.data, status=status.HTTP_201_CREATED)
-class VehicleDetailApiView(APIView):
- """
- Retrieve, update or delete a vehicle instance.
- """
-
- serializer_class = VehicleSerializer
-
+class DetailApiView(APIView):
def get_permissions(self):
- if self.request.method == "PUT" or self.request.method == "DELETE":
- self.permission_classes = [IsAdminUser]
+ if self.request.method in ["DELETE"]:
+ self.permission_classes = [permissions.IsAdminUser]
else:
- self.permission_classes = [IsAuthenticated]
+ self.permission_classes = [permissions.IsAuthenticated]
return super().get_permissions()
- def get(self, request, vehicle_id, *args, **kwargs):
- """
- Get specific vehicle
- """
- try:
- vehicle = get_object_or_404(Vehicle, id=vehicle_id)
- serializer = VehicleSerializer(vehicle)
- return Response(serializer.data, status=status.HTTP_200_OK)
- except vehicle.DoesNotExist:
- return Response(status=status.HTTP_404_NOT_FOUND)
+ def get_serializer_class(self, item_type):
+ serializer_map = {
+ "truck": VehicleSerializer,
+ "equipment": EquipmentSerializer,
+ "trailer": TrailerSerializer,
+ }
+ return serializer_map.get(item_type, VehicleSerializer)
+
+ def get_object(self, item_id, item_type):
+ model_map = {
+ "truck": Vehicle,
+ "equipment": Equipment,
+ "trailer": Trailer,
+ }
+ model = model_map.get(item_type, Vehicle)
+ return get_object_or_404(model, id=item_id)
+
+ def get(self, request, item_id, *args, **kwargs):
+ item_type = request.query_params.get("type", "truck")
+ item = self.get_object(item_id, item_type)
+ serializer_class = self.get_serializer_class(item_type)
+ serializer = serializer_class(item)
+ return Response(serializer.data, status=status.HTTP_200_OK)
- def put(self, request, vehicle_id, format=None):
- """
- Update specific vehicle
- """
- vehicle = get_object_or_404(Vehicle, id=vehicle_id)
- serializer = VehicleSerializer(vehicle, data=request.data)
+ def put(self, request, item_id, *args, **kwargs):
+ item_type = request.query_params.get("type", "truck")
+ item = self.get_object(item_id, item_type)
+ serializer_class = self.get_serializer_class(item_type)
+ serializer = serializer_class(item, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- def delete(self, request, vehicle_id, format=None):
- """
- Delete specific vehicle
- """
- vehicle = get_object_or_404(Vehicle, id=vehicle_id)
- vehicle.delete()
+ def delete(self, request, item_id, *args, **kwargs):
+ item_type = request.query_params.get("type", "truck")
+ item = self.get_object(item_id, item_type)
+ item.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
-class VehicleFilterList(APIView):
- """
- Get list of vehicles based off of filter
- Takes limit + offset from url
- """
-
- permission_classes = [IsAuthenticated]
-
- def get_queryset(self):
- queryset = infinite_filter(self.request)
- return queryset
-
- def get(self, request):
- url_parameter = request.GET.get("search")
- if url_parameter:
- vehicles = self.get_queryset()
-
- serialized_data = VehicleSerializer(vehicles, many=True)
-
- return Response(
- {"vehicles": serialized_data.data, "more_data": has_more_data(request)}
- )
-
- return Response(VehicleSerializer(Vehicle.objects.all()[:10], many=True).data)
-
-
-class VehiclePriceApiView(APIView):
- """
- Update a vehicle's minimum price
- """
-
- permission_classes = [IsAdminUser]
-
- serializer_class = VehicleSerializer
-
- def put(self, request, vehicle_id, format=None):
- """
- Update specific vehicle price
- """
- vehicle = get_object_or_404(Vehicle, id=vehicle_id)
-
- new_price = request.data.get("minimum_price")
- if new_price is None:
- return Response(
- {"error": "Must provide minimum price"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- serializer = VehicleSerializer(
- vehicle, data={"minimum_price": new_price}, partial=True
- )
- if serializer.is_valid():
- serializer.save()
- return Response(serializer.data)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
-
class UploadFileView(APIView):
permission_classes = [permissions.AllowAny]
authentication_classes = []
@@ -172,3 +178,21 @@ def post(self, request):
except Exception as e:
error_message = str(e)
return Response({"status": "error", "message": error_message})
+
+
+class BrandListApiView(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def get(self, request, *args, **kwargs):
+ brands = Brand.objects.all()
+ serialized_data = BrandSerializer(brands, many=True).data
+ return Response(serialized_data, status=status.HTTP_200_OK)
+
+
+class TypeListApiView(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def get(self, request, *args, **kwargs):
+ types = Type.objects.all()
+ serialized_data = TypeSerializer(types, many=True).data
+ return Response(serialized_data, status=status.HTTP_200_OK)
diff --git a/frontend/src/components/cards/NotificationPopUpCard.jsx b/frontend/src/components/cards/NotificationPopUpCard.jsx
index 6c4f382..6f8de87 100644
--- a/frontend/src/components/cards/NotificationPopUpCard.jsx
+++ b/frontend/src/components/cards/NotificationPopUpCard.jsx
@@ -76,9 +76,9 @@ export default function NotificationPopUpCard() {
1 hour left until auction ends
-
+
Put in your bids by 21:59!
-
+
@@ -109,9 +109,9 @@ export default function NotificationPopUpCard() {
You have been verified
-
+
You are now registered to make bids
-
+
diff --git a/frontend/src/components/dropdowns/Dropdown.jsx b/frontend/src/components/dropdowns/Dropdown.jsx
index 6f41871..850fda5 100644
--- a/frontend/src/components/dropdowns/Dropdown.jsx
+++ b/frontend/src/components/dropdowns/Dropdown.jsx
@@ -51,7 +51,7 @@ export default function Dropdown({ title, items, onValueChange }) {
{isOpen && (
item.name !== 'All'),
+ ];
+
const handleValueChange = (newValue) => {
setSelectedValue(newValue);
setIsOpen(false);
@@ -29,10 +34,7 @@ export default function FilterDropdown({ title, items, onValueChange }) {
}, [isOpen]);
return (
-
+
diff --git a/frontend/src/components/dropdowns/SortByDropdown.jsx b/frontend/src/components/dropdowns/SortByDropdown.jsx
index f7c4eab..069438e 100644
--- a/frontend/src/components/dropdowns/SortByDropdown.jsx
+++ b/frontend/src/components/dropdowns/SortByDropdown.jsx
@@ -32,7 +32,7 @@ export default function SortByDropdown({ title, items, onValueChange }) {
setIsOpen(!isOpen)}
@@ -48,7 +48,7 @@ export default function SortByDropdown({ title, items, onValueChange }) {
{isOpen && (
+
₱
{
- if (!input.trim()) return;
-
- try {
- const response = await fetchData({
- url: `/v1/vehicles?search=${input}`,
- method: 'GET',
- });
- setResults(response.data);
- } catch (err) {
- // eslint-disable-next-line no-console
- console.error(err.message || 'An unknown error occurred');
- }
- };
-
- useEffect(() => {
- const delayDebounceFn = setTimeout(() => {
- if (input) {
- handleFetchVehicles();
- }
- }, 500);
-
- return () => clearTimeout(delayDebounceFn);
- }, [input]);
-
- const handleChange = (value) => {
- setInput(value);
- };
+export default function ListingSearchBar({ input, setInput }) {
return (
@@ -43,7 +9,7 @@ export default function ListingSearchBar({ setResults }) {
className="w-full text-base font-normal text-mv-black placeholder-dark-grey outline-none leading-6 tracking-[0.5px]"
placeholder="Search vehicle"
value={input}
- onChange={(e) => handleChange(e.target.value)}
+ onChange={(e) => setInput(e.target.value)}
/>
);
diff --git a/frontend/src/pages/ListingsPage.jsx b/frontend/src/pages/ListingsPage.jsx
index 24c19fd..2fb3506 100644
--- a/frontend/src/pages/ListingsPage.jsx
+++ b/frontend/src/pages/ListingsPage.jsx
@@ -1,5 +1,8 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useCallback } from 'react';
import { ScrollRestoration } from 'react-router-dom';
+import { debounce } from 'lodash';
+import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
+import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft';
import NavBar from '../components/navBars/NavBar';
import CurrentAuctionCountdown from '../components/timers/CurrentAuctionCountdown';
import ListingSearchBar from '../components/searchBars/ListingsSearchBar';
@@ -10,6 +13,7 @@ import vehicleImage from '../assets/truck.png';
import Footer from '../components/footers/Footer';
import PriceInputField from '../components/inputs/PriceInputField';
import useAxios from '../hooks/useAxios';
+import { priceToString } from '../utils/priceUtil';
import {
formatListingsTodayDate,
formatFlexibleDateRange,
@@ -17,16 +21,19 @@ import {
} from '../utils/dateTime';
export default function ListingsPage() {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [totalPages, setTotalPages] = useState(0);
+ // eslint-disable-next-line no-unused-vars
+ const [loading, setLoading] = useState(false);
+ const [hasNextPage, setHasNextPage] = useState(null);
+ const [hasPrevPage, setHasPrevPage] = useState(null);
+
const [auction, setAuction] = useState(null);
+ const [auctionDayId, setAuctionDayId] = useState(null);
const [units, setUnits] = useState([]);
+ const [searchTerm, setSearchTerm] = useState('');
const [brands, setBrands] = useState([]);
const [types, setTypes] = useState([]);
- const [sortByItems, setSortByItems] = useState([
- 'All',
- 'Trucks',
- 'Equipments',
- 'Trailers',
- ]);
const [selectedSortBy, setSelectedSortBy] = useState('All');
const [selectedType, setSelectedType] = useState('All');
const [selectedBrand, setSelectedBrand] = useState('All');
@@ -34,6 +41,8 @@ export default function ListingsPage() {
const [selectedMaxPrice, setSelectedMaxPrice] = useState(0);
const { fetchData } = useAxios();
+ const sortByItems = ['All', 'Trucks', 'Equipment', 'Trailers'];
+
const updateMinPrice = ({ target: { value } }) => {
if (value === '') {
setSelectedMinPrice(0);
@@ -52,14 +61,41 @@ export default function ListingsPage() {
useEffect(() => {
const fetchAuctionAndVehicles = async () => {
+ setLoading(true);
try {
- const currentAuctionResponse = await fetchData({
+ const brandsPromise = fetchData({
+ endpoint: '/v1/vehicles/brands',
+ method: 'GET',
+ });
+
+ const typesPromise = fetchData({
+ endpoint: '/v1/vehicles/types',
+ method: 'GET',
+ });
+
+ const currentAuctionPromise = fetchData({
endpoint: '/v1/auctions/current',
method: 'GET',
});
+
+ const [brandsResponse, typesResponse, currentAuctionResponse] =
+ await Promise.all([
+ brandsPromise,
+ typesPromise,
+ currentAuctionPromise,
+ ]);
+
+ setBrands(
+ brandsResponse.data.filter((brand) => brand && brand.name !== 'nan')
+ );
+ setTypes(
+ typesResponse.data.filter((type) => type && type.name !== 'nan')
+ );
+
const currentAuction = currentAuctionResponse.data;
if (!currentAuction.id) {
+ setLoading(false);
return;
}
@@ -70,64 +106,133 @@ export default function ListingsPage() {
method: 'GET',
});
- const auctionDayId = dateResponse.data.find((day) => {
+ const dayId = dateResponse.data.find((day) => {
const requestDate = new Date(day.date).setHours(0, 0, 0, 0);
const currentDate = new Date().setHours(0, 0, 0, 0);
return requestDate === currentDate;
})?.id;
- if (!auctionDayId) {
+ setAuctionDayId(dayId);
+
+ if (!dayId) {
+ setLoading(false);
return;
}
- const brandSet = new Set();
- const typeSet = new Set();
-
const vehiclesResponse = await fetchData({
- endpoint: `/v1/auctions/${currentAuction.id}/days/${auctionDayId}/vehicles`,
+ endpoint: `/v1/auctions/${currentAuction.id}/days/${dayId}/vehicles`,
method: 'GET',
});
- setSortByItems(
- ['All', 'Trucks', 'Equipments', 'Trailers'].filter((item) => {
- if (item === 'Trucks') {
- return vehiclesResponse.data.vehicles.length > 0;
- }
- if (item === 'Equipments') {
- return vehiclesResponse.data.equipment.length > 0;
- }
- if (item === 'Trailers') {
- return vehiclesResponse.data.trailers.length > 0;
- }
- return true;
- })
- );
-
- const vehicleIds = vehiclesResponse.data.vehicles;
-
- vehicleIds.forEach(async (vehicle) => {
- const vehicleResponse = await fetchData({
- endpoint: `/v1/vehicles/${vehicle.id}`,
- method: 'GET',
- });
-
- const vehicleData = vehicleResponse.data;
-
- setUnits((prevUnits) => [...prevUnits, vehicleData]);
-
- brandSet.add(vehicleData.brand_name);
- typeSet.add(vehicleData.vehicle_type_name);
- setBrands(['All', ...brandSet]);
- setTypes(['All', ...typeSet]);
- });
+ setUnits(vehiclesResponse.data.results);
+ setHasNextPage(vehiclesResponse.data.next);
+ setHasPrevPage(vehiclesResponse.data.previous);
+ setTotalPages(Math.ceil(vehiclesResponse.data.count / 5));
} catch (error) {
/* empty */
+ } finally {
+ setLoading(false);
}
};
fetchAuctionAndVehicles();
}, []);
+ const handleFetchVehicles = useCallback(
+ debounce(async (url) => {
+ setLoading(true);
+ try {
+ const response = await fetchData({
+ endpoint: url,
+ method: 'GET',
+ });
+ setUnits(response.data.results);
+ setHasNextPage(response.data.next);
+ setHasPrevPage(response.data.previous);
+ setTotalPages(Math.ceil(response.data.count / 5));
+ } catch (error) {
+ /* empty */
+ } finally {
+ setLoading(false);
+ }
+ }, 500),
+ [auction?.id, currentPage]
+ );
+
+ const generateUrlFromFilters = (url) => {
+ let filtersString = '';
+ if (selectedSortBy !== 'All') {
+ filtersString += `&item_type=${selectedSortBy.toLowerCase()}`;
+ }
+
+ if (selectedType !== 'All' && selectedType?.name !== 'All') {
+ filtersString += `&type=${selectedType.id}`;
+ }
+
+ if (selectedBrand !== 'All' && selectedBrand?.name !== 'All') {
+ filtersString += `&brand=${selectedBrand.id}`;
+ }
+
+ if (selectedMinPrice > 0) {
+ filtersString += `&min_price=${selectedMinPrice}`;
+ }
+
+ if (selectedMaxPrice > 0) {
+ filtersString += `&max_price=${selectedMaxPrice}`;
+ }
+
+ if (searchTerm && searchTerm !== '') {
+ filtersString += `&search=${encodeURIComponent(searchTerm)}`;
+ }
+
+ return url + filtersString;
+ };
+
+ const handleNextPage = () => {
+ if (hasNextPage) {
+ handleFetchVehicles(
+ generateUrlFromFilters(
+ `v1/auctions/${auction?.id}/days/${auctionDayId}/vehicles?page=${
+ currentPage + 1
+ }`
+ )
+ );
+ setCurrentPage((prev) => prev + 1);
+ window.scrollTo(0, 0);
+ }
+ };
+
+ const handlePreviousPage = () => {
+ if (hasPrevPage) {
+ handleFetchVehicles(
+ generateUrlFromFilters(
+ `v1/auctions/${auction?.id}/days/${auctionDayId}/vehicles?page=${
+ currentPage - 1
+ }`
+ )
+ );
+ setCurrentPage((prev) => prev + 1);
+ window.scrollTo(0, 0);
+ }
+ };
+
+ useEffect(() => {
+ if (auction && auctionDayId) {
+ handleFetchVehicles(
+ generateUrlFromFilters(
+ `/v1/auctions/${auction?.id}/days/${auctionDayId}/vehicles?page=${currentPage}`
+ )
+ );
+ }
+ }, [
+ selectedSortBy,
+ selectedType,
+ selectedBrand,
+ selectedMinPrice,
+ selectedMaxPrice,
+ searchTerm,
+ ]);
+
if (
!auction ||
new Date().getTime() < new Date(auction?.start_date).getTime()
@@ -190,11 +295,11 @@ export default function ListingsPage() {
-
+
-
+
Filters
@@ -215,7 +320,7 @@ export default function ListingsPage() {
Price
-
+
+
{units.map((vehicle) => (
))}
+
+
+
+ {hasPrevPage && (
+
+ )}
+ {hasNextPage && (
+
+ )}
+
+
{`Page ${currentPage} of ${totalPages}`}
+
-
diff --git a/frontend/src/utils/priceUtil.js b/frontend/src/utils/priceUtil.js
new file mode 100644
index 0000000..b578474
--- /dev/null
+++ b/frontend/src/utils/priceUtil.js
@@ -0,0 +1,4 @@
+// eslint-disable-next-line import/prefer-default-export
+export function priceToString(price) {
+ return price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+}