diff --git a/docs/docs/assets/images/build/build_calendar.png b/docs/docs/assets/images/build/build_calendar.png
new file mode 100644
index 0000000000..07047a19e5
Binary files /dev/null and b/docs/docs/assets/images/build/build_calendar.png differ
diff --git a/docs/docs/assets/images/build/build_display.png b/docs/docs/assets/images/build/build_display.png
index c8d01916b3..bcfa770905 100644
Binary files a/docs/docs/assets/images/build/build_display.png and b/docs/docs/assets/images/build/build_display.png differ
diff --git a/docs/docs/assets/images/build/build_list.png b/docs/docs/assets/images/build/build_list.png
index d9003a6729..236d7a4a1e 100644
Binary files a/docs/docs/assets/images/build/build_list.png and b/docs/docs/assets/images/build/build_list.png differ
diff --git a/docs/docs/assets/images/order/po_calendar.png b/docs/docs/assets/images/order/po_calendar.png
new file mode 100644
index 0000000000..181009f290
Binary files /dev/null and b/docs/docs/assets/images/order/po_calendar.png differ
diff --git a/docs/docs/assets/images/order/po_display.png b/docs/docs/assets/images/order/po_display.png
new file mode 100644
index 0000000000..9f928d4771
Binary files /dev/null and b/docs/docs/assets/images/order/po_display.png differ
diff --git a/docs/docs/assets/images/order/po_list.png b/docs/docs/assets/images/order/po_list.png
index 6b1b5349e3..1cd809ea23 100644
Binary files a/docs/docs/assets/images/order/po_list.png and b/docs/docs/assets/images/order/po_list.png differ
diff --git a/docs/docs/assets/images/order/ro_calendar.png b/docs/docs/assets/images/order/ro_calendar.png
new file mode 100644
index 0000000000..cdb87a0457
Binary files /dev/null and b/docs/docs/assets/images/order/ro_calendar.png differ
diff --git a/docs/docs/assets/images/order/ro_display.png b/docs/docs/assets/images/order/ro_display.png
new file mode 100644
index 0000000000..4580c54deb
Binary files /dev/null and b/docs/docs/assets/images/order/ro_display.png differ
diff --git a/docs/docs/assets/images/order/ro_list.png b/docs/docs/assets/images/order/ro_list.png
new file mode 100644
index 0000000000..2adf57e5c8
Binary files /dev/null and b/docs/docs/assets/images/order/ro_list.png differ
diff --git a/docs/docs/assets/images/order/so_calendar.png b/docs/docs/assets/images/order/so_calendar.png
new file mode 100644
index 0000000000..428f420fd1
Binary files /dev/null and b/docs/docs/assets/images/order/so_calendar.png differ
diff --git a/docs/docs/assets/images/order/so_display.png b/docs/docs/assets/images/order/so_display.png
new file mode 100644
index 0000000000..2b6204e578
Binary files /dev/null and b/docs/docs/assets/images/order/so_display.png differ
diff --git a/docs/docs/assets/images/order/so_list.png b/docs/docs/assets/images/order/so_list.png
index 9104a21483..5ef076b6f9 100644
Binary files a/docs/docs/assets/images/order/so_list.png and b/docs/docs/assets/images/order/so_list.png differ
diff --git a/docs/docs/build/build.md b/docs/docs/build/build.md
index d800f153a6..cfa1931bdf 100644
--- a/docs/docs/build/build.md
+++ b/docs/docs/build/build.md
@@ -12,12 +12,16 @@ A *Build Order* uses the BOM to allocate stock items to the assembly process. As
 
 ### View Build Orders
 
-To navigate to the Build Order display, select *Build* from the main navigation menu:
+To navigate to the Build Order display, select *Manufacturing* from the main navigation menu, and *Build Orders* from the sidebar.
+
+The *Build Order Index Page* allows the user to view all build orders:
 
 {% with id="build_display", url="build/build_display.png", description="Display Builds" %}
 {% include "img.html" %}
 {% endwith %}
 
+The following view modes are available:
+
 #### Table View
 
 *Table View* provides a table of Build Orders, which can be filtered to only show the orders you are interested in.
@@ -28,7 +32,13 @@ To navigate to the Build Order display, select *Build* from the main navigation
 
 #### Calendar View
 
-*Calendar View* shows a calendar display with upcoming build orders, based on the various dates specified for each build.
+*Calendar View* shows a calendar display with outstanding build orders, based on the various dates specified for each order.
+
+{% with id="build_calendar", url="build/build_calendar.png", description="Build Calendar" %}
+{% include "img.html" %}
+{% endwith %}
+
+The build calendar allows the user to navigate month-by-month and display the filtered build orders
 
 ## Build Order Details
 
diff --git a/docs/docs/order/purchase_order.md b/docs/docs/order/purchase_order.md
index f2d61eddb2..e78bc2c54e 100644
--- a/docs/docs/order/purchase_order.md
+++ b/docs/docs/order/purchase_order.md
@@ -6,12 +6,32 @@ title: Purchase Order
 
 Purchase orders allow to track which parts are bought from suppliers and manufacturers, therefore converting externally bought items into stock items / inventory.
 
-To access the purchase order page, click on the <span class="badge inventree nav main"><span class='fas fa-shopping-cart'></span> Buy</span> navigation tab and click on <span class="badge inventree nav main"><span class='fas fa-list'></span> Purchase Orders</span> option in the dropdown list.
+### View Purchase Orders
+
+To navigate to the Purchase Order display, select *Purchasing* from the main navigation menu, and *Build Orders* from the sidebar:
+
+{% with id="purchase_order_display", url="order/po_display.png", description="Purchase Order Display" %}
+{% include "img.html" %}
+{% endwith %}
+
+The following view modes are available:
+
+#### Table View
+
+*Table View* provides a list of Purchase Orders, which can be filtered to display a subset of orders according to user supplied parameters.
 
 {% with id="purchase_order_list", url="order/po_list.png", description="Purchase Order List" %}
 {% include "img.html" %}
 {% endwith %}
 
+#### Calendar View
+
+*Calendar View* shows a calendar display with outstanding purchase orders, based on the various dates specified for each order.
+
+{% with id="purchase_order_calendar", url="order/po_calendar.png", description="Purchase Order Calendar" %}
+{% include "img.html" %}
+{% endwith %}
+
 ### Purchase Order Status Codes
 
 Each Purchase Order has a specific status code which indicates the current state of the order:
diff --git a/docs/docs/order/return_order.md b/docs/docs/order/return_order.md
index a188dd076f..d2e3a3bfe3 100644
--- a/docs/docs/order/return_order.md
+++ b/docs/docs/order/return_order.md
@@ -9,6 +9,32 @@ Return Orders allow stock items (which have been sold or allocated to a customer
 !!! tip "An Order By Any Other Name"
     A Return Order may also be known as an [RMA](https://en.wikipedia.org/wiki/Return_merchandise_authorization)
 
+### View Return Orders
+
+To navigate to the Return Order display, select *Sales* from the main navigation menu, and *Return Orders* from the sidebar:
+
+{% with id="return_order_display", url="order/ro_display.png", description="Return Order Display" %}
+{% include "img.html" %}
+{% endwith %}
+
+The following view modes are available:
+
+#### Table View
+
+*Table View* provides a list of Return Orders, which can be filtered to display a subset of orders according to user supplied parameters.
+
+{% with id="purchase_order_list", url="order/po_list.png", description="Return Order List" %}
+{% include "img.html" %}
+{% endwith %}
+
+#### Calendar View
+
+*Calendar View* shows a calendar display with outstanding return orders, based on the various dates specified for each order.
+
+{% with id="return_order_calendar", url="order/ro_calendar.png", description="Return Order Calendar" %}
+{% include "img.html" %}
+{% endwith %}
+
 ### Enable Return Order Functionality
 
 By default, Return Order functionality is not enabled - it must be enabled by a *staff* user from the settings page:
diff --git a/docs/docs/order/sales_order.md b/docs/docs/order/sales_order.md
index fca9a46f7b..1923ea5d9f 100644
--- a/docs/docs/order/sales_order.md
+++ b/docs/docs/order/sales_order.md
@@ -6,12 +6,32 @@ title: Sales Orders
 
 Sales orders allow tracking of which stock items are sold to customers, therefore converting stock items / inventory into externally sold items.
 
-To access the sales order page, click on the <span class="badge inventree nav main"><span class='fas fa-truck'></span> Sell</span> navigation tab and click on <span class="badge inventree nav main"><span class='fas fa-list'></span> Sales Orders</span> option in the dropdown list.
+### View Sales Orders
+
+To navigate to the Sales Order display, select *Sales* from the main navigation menu, and *Sales Orders* from the sidebar:
+
+{% with id="sales_order_display", url="order/so_display.png", description="Sales Order Display" %}
+{% include "img.html" %}
+{% endwith %}
+
+The following view modes are available:
+
+#### Table View
+
+*Table View* provides a list of Sales Orders, which can be filtered to display a subset of orders according to user supplied parameters.
 
 {% with id="sales_order_list", url="order/so_list.png", description="Sales Order List" %}
 {% include "img.html" %}
 {% endwith %}
 
+#### Calendar View
+
+*Calendar View* shows a calendar display with outstanding sales orders.
+
+{% with id="sales_order_calendar", url="order/so_calendar.png", description="Sales Order Calendar" %}
+{% include "img.html" %}
+{% endwith %}
+
 ### Sales Order Status Codes
 
 Each Sales Order has a specific status code, which represents the state of the order:
diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py
index d918dc2b06..cf06a17cfb 100644
--- a/src/backend/InvenTree/InvenTree/api_version.py
+++ b/src/backend/InvenTree/InvenTree/api_version.py
@@ -1,13 +1,16 @@
 """InvenTree API version information."""
 
 # InvenTree API version
-INVENTREE_API_VERSION = 321
+INVENTREE_API_VERSION = 322
 
 """Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
 
 
 INVENTREE_API_TEXT = """
 
+v322 - 2025-03-16 : https://github.com/inventree/InvenTree/pull/8933
+    - Add min_date and max_date query filters for orders, for use in calendar views
+
 v321 - 2025-03-06 : https://github.com/inventree/InvenTree/pull/9236
     - Adds conditionally-returned fields to the schema to match API behavior
     - Removes required flag for nullable read-only fields to match API behavior
diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py
index 4f8274b5ea..c902abe12f 100644
--- a/src/backend/InvenTree/build/api.py
+++ b/src/backend/InvenTree/build/api.py
@@ -242,6 +242,65 @@ class BuildFilter(rest_filters.FilterSet):
         label=_('Completed after'), field_name='completion_date', lookup_expr='gt'
     )
 
+    min_date = InvenTreeDateFilter(label=_('Min Date'), method='filter_min_date')
+
+    def filter_min_date(self, queryset, name, value):
+        """Filter the queryset to include orders *after* a specified date.
+
+        This filter is used in combination with filter_max_date,
+        to provide a queryset which matches a particular range of dates.
+
+        In particular, this is used in the UI for the calendar view.
+
+        So, we are interested in orders which are active *after* this date:
+
+        - creation_date is set *after* this date (but there is no start date)
+        - start_date is set *after* this date
+        - target_date is set *after* this date
+
+        """
+        q1 = Q(creation_date__gte=value, start_date__isnull=True)
+        q2 = Q(start_date__gte=value)
+        q3 = Q(target_date__gte=value)
+
+        return queryset.filter(q1 | q2 | q3).distinct()
+
+    max_date = InvenTreeDateFilter(label=_('Max Date'), method='filter_max_date')
+
+    def filter_max_date(self, queryset, name, value):
+        """Filter the queryset to include orders *before* a specified date.
+
+        This filter is used in combination with filter_min_date,
+        to provide a queryset which matches a particular range of dates.
+
+        In particular, this is used in the UI for the calendar view.
+
+        So, we are interested in orders which are active *before* this date:
+
+        - creation_date is set *before* this date (but there is no start date)
+        - start_date is set *before* this date
+        - target_date is set *before* this date
+        """
+        q1 = Q(creation_date__lte=value, start_date__isnull=True)
+        q2 = Q(start_date__lte=value)
+        q3 = Q(target_date__lte=value)
+
+        return queryset.filter(q1 | q2 | q3).distinct()
+
+    exclude_tree = rest_filters.ModelChoiceFilter(
+        queryset=Build.objects.all(),
+        method='filter_exclude_tree',
+        label=_('Exclude Tree'),
+    )
+
+    def filter_exclude_tree(self, queryset, name, value):
+        """Filter by excluding a tree of Build objects."""
+        queryset = queryset.exclude(
+            pk__in=[bld.pk for bld in value.get_descendants(include_self=True)]
+        )
+
+        return queryset
+
 
 class BuildMixin:
     """Mixin class for Build API endpoints."""
@@ -319,35 +378,6 @@ class BuildList(DataExportViewMixin, BuildMixin, ListCreateAPI):
 
         return queryset
 
-    def filter_queryset(self, queryset):
-        """Custom query filtering for the BuildList endpoint."""
-        queryset = super().filter_queryset(queryset)
-
-        params = self.request.query_params
-
-        # exclude parent tree
-        exclude_tree = params.get('exclude_tree', None)
-
-        if exclude_tree is not None:
-            try:
-                build = Build.objects.get(pk=exclude_tree)
-
-                queryset = queryset.exclude(
-                    pk__in=[bld.pk for bld in build.get_descendants(include_self=True)]
-                )
-
-            except (ValueError, Build.DoesNotExist):
-                pass
-
-        # Filter by 'date range'
-        min_date = params.get('min_date', None)
-        max_date = params.get('max_date', None)
-
-        if min_date is not None and max_date is not None:
-            queryset = Build.filterByDate(queryset, min_date, max_date)
-
-        return queryset
-
     def get_serializer(self, *args, **kwargs):
         """Add extra context information to the endpoint serializer."""
         try:
diff --git a/src/backend/InvenTree/build/models.py b/src/backend/InvenTree/build/models.py
index 0002620112..ab2d0663dc 100644
--- a/src/backend/InvenTree/build/models.py
+++ b/src/backend/InvenTree/build/models.py
@@ -1,7 +1,6 @@
 """Build database model definitions."""
 
 import decimal
-from datetime import datetime
 
 from django.contrib.auth.models import User
 from django.core.exceptions import ValidationError
@@ -197,44 +196,6 @@ class Build(
             'title': str(self),
         }
 
-    @staticmethod
-    def filterByDate(queryset, min_date, max_date):
-        """Filter by 'minimum and maximum date range'.
-
-        - Specified as min_date, max_date
-        - Both must be specified for filter to be applied
-        """
-        date_fmt = '%Y-%m-%d'  # ISO format date string
-
-        # Ensure that both dates are valid
-        try:
-            min_date = datetime.strptime(str(min_date), date_fmt).date()
-            max_date = datetime.strptime(str(max_date), date_fmt).date()
-        except (ValueError, TypeError):
-            # Date processing error, return queryset unchanged
-            return queryset
-
-        # Order was completed within the specified range
-        completed = (
-            Q(status=BuildStatus.COMPLETE.value)
-            & Q(completion_date__gte=min_date)
-            & Q(completion_date__lte=max_date)
-        )
-
-        # Order target date falls within specified range
-        pending = (
-            Q(status__in=BuildStatusGroups.ACTIVE_CODES)
-            & ~Q(target_date=None)
-            & Q(target_date__gte=min_date)
-            & Q(target_date__lte=max_date)
-        )
-
-        # TODO - Construct a queryset for "overdue" orders
-
-        queryset = queryset.filter(completed | pending)
-
-        return queryset
-
     def __str__(self):
         """String representation of a BuildOrder."""
         return self.reference
diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py
index 031a5bf575..71fc522886 100644
--- a/src/backend/InvenTree/order/api.py
+++ b/src/backend/InvenTree/order/api.py
@@ -213,6 +213,44 @@ class OrderFilter(rest_filters.FilterSet):
         label=_('Target Date After'), field_name='target_date', lookup_expr='gt'
     )
 
+    min_date = InvenTreeDateFilter(label=_('Min Date'), method='filter_min_date')
+
+    def filter_min_date(self, queryset, name, value):
+        """Filter the queryset to include orders *after* a specified date.
+
+        This is used in combination with filter_max_date,
+        to provide a queryset which matches a particular range of dates.
+
+        In particular, this is used in the UI for the calendar view.
+        """
+        q1 = Q(
+            creation_date__gte=value, issue_date__isnull=True, start_date__isnull=True
+        )
+        q2 = Q(issue_date__gte=value, start_date__isnull=True)
+        q3 = Q(start_date__gte=value)
+        q4 = Q(target_date__gte=value)
+
+        return queryset.filter(q1 | q2 | q3 | q4).distinct()
+
+    max_date = InvenTreeDateFilter(label=_('Max Date'), method='filter_max_date')
+
+    def filter_max_date(self, queryset, name, value):
+        """Filter the queryset to include orders *before* a specified date.
+
+        This is used in combination with filter_min_date,
+        to provide a queryset which matches a particular range of dates.
+
+        In particular, this is used in the UI for the calendar view.
+        """
+        q1 = Q(
+            creation_date__lte=value, issue_date__isnull=True, start_date__isnull=True
+        )
+        q2 = Q(issue_date__lte=value, start_date__isnull=True)
+        q3 = Q(start_date__lte=value)
+        q4 = Q(target_date__lte=value)
+
+        return queryset.filter(q1 | q2 | q3 | q4).distinct()
+
 
 class LineItemFilter(rest_filters.FilterSet):
     """Base class for custom API filters for order line item list(s)."""
@@ -323,23 +361,6 @@ class PurchaseOrderList(
     """
 
     filterset_class = PurchaseOrderFilter
-
-    def filter_queryset(self, queryset):
-        """Custom queryset filtering."""
-        # Perform basic filtering
-        queryset = super().filter_queryset(queryset)
-
-        params = self.request.query_params
-
-        # Filter by 'date range'
-        min_date = params.get('min_date', None)
-        max_date = params.get('max_date', None)
-
-        if min_date is not None and max_date is not None:
-            queryset = models.PurchaseOrder.filterByDate(queryset, min_date, max_date)
-
-        return queryset
-
     filter_backends = SEARCH_ORDER_FILTER_ALIAS
 
     ordering_field_aliases = {
@@ -791,21 +812,6 @@ class SalesOrderList(
 
     filterset_class = SalesOrderFilter
 
-    def filter_queryset(self, queryset):
-        """Perform custom filtering operations on the SalesOrder queryset."""
-        queryset = super().filter_queryset(queryset)
-
-        params = self.request.query_params
-
-        # Filter by 'date range'
-        min_date = params.get('min_date', None)
-        max_date = params.get('max_date', None)
-
-        if min_date is not None and max_date is not None:
-            queryset = models.SalesOrder.filterByDate(queryset, min_date, max_date)
-
-        return queryset
-
     filter_backends = SEARCH_ORDER_FILTER_ALIAS
 
     ordering_field_aliases = {
diff --git a/src/backend/InvenTree/order/migrations/0109_salesorder_issue_date.py b/src/backend/InvenTree/order/migrations/0109_salesorder_issue_date.py
new file mode 100644
index 0000000000..4d8efd0626
--- /dev/null
+++ b/src/backend/InvenTree/order/migrations/0109_salesorder_issue_date.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.19 on 2025-02-23 04:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("order", "0108_alter_purchaseorder_link_and_more_squashed_0109_alter_purchaseorderextraline_link_and_more"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="salesorder",
+            name="issue_date",
+            field=models.DateField(
+                blank=True,
+                help_text="Date order was issued",
+                null=True,
+                verbose_name="Issue Date",
+            ),
+        ),
+    ]
diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py
index b04776a36f..d7c299bf8c 100644
--- a/src/backend/InvenTree/order/models.py
+++ b/src/backend/InvenTree/order/models.py
@@ -1,6 +1,5 @@
 """Order model definitions."""
 
-from datetime import datetime
 from decimal import Decimal
 
 from django.contrib.auth.models import User
@@ -389,6 +388,13 @@ class Order(
         verbose_name=_('Created By'),
     )
 
+    issue_date = models.DateField(
+        blank=True,
+        null=True,
+        verbose_name=_('Issue Date'),
+        help_text=_('Date order was issued'),
+    )
+
     responsible = models.ForeignKey(
         UserModels.Owner,
         on_delete=models.SET_NULL,
@@ -482,50 +488,6 @@ class PurchaseOrder(TotalPriceMixin, Order):
         """Return the associated barcode model type code for this model."""
         return 'PO'
 
-    @staticmethod
-    def filterByDate(queryset, min_date, max_date):
-        """Filter by 'minimum and maximum date range'.
-
-        - Specified as min_date, max_date
-        - Both must be specified for filter to be applied
-        - Determine which "interesting" orders exist between these dates
-
-        To be "interesting":
-        - A "received" order where the received date lies within the date range
-        - A "pending" order where the target date lies within the date range
-        - TODO: An "overdue" order where the target date is in the past
-        """
-        date_fmt = '%Y-%m-%d'  # ISO format date string
-
-        # Ensure that both dates are valid
-        try:
-            min_date = datetime.strptime(str(min_date), date_fmt).date()
-            max_date = datetime.strptime(str(max_date), date_fmt).date()
-        except (ValueError, TypeError):
-            # Date processing error, return queryset unchanged
-            return queryset
-
-        # Construct a queryset for "received" orders within the range
-        received = (
-            Q(status=PurchaseOrderStatus.COMPLETE.value)
-            & Q(complete_date__gte=min_date)
-            & Q(complete_date__lte=max_date)
-        )
-
-        # Construct a queryset for "pending" orders within the range
-        pending = (
-            Q(status__in=PurchaseOrderStatusGroups.OPEN)
-            & ~Q(target_date=None)
-            & Q(target_date__gte=min_date)
-            & Q(target_date__lte=max_date)
-        )
-
-        # TODO - Construct a queryset for "overdue" orders within the range
-
-        queryset = queryset.filter(received | pending)
-
-        return queryset
-
     def __str__(self):
         """Render a string representation of this PurchaseOrder."""
         return f'{self.reference} - {self.supplier.name if self.supplier else _("deleted")}'
@@ -584,13 +546,6 @@ class PurchaseOrder(TotalPriceMixin, Order):
         verbose_name=_('received by'),
     )
 
-    issue_date = models.DateField(
-        blank=True,
-        null=True,
-        verbose_name=_('Issue Date'),
-        help_text=_('Date order was issued'),
-    )
-
     complete_date = models.DateField(
         blank=True,
         null=True,
@@ -1054,50 +1009,6 @@ class SalesOrder(TotalPriceMixin, Order):
         """Return the associated barcode model type code for this model."""
         return 'SO'
 
-    @staticmethod
-    def filterByDate(queryset, min_date, max_date):
-        """Filter by "minimum and maximum date range".
-
-        - Specified as min_date, max_date
-        - Both must be specified for filter to be applied
-        - Determine which "interesting" orders exist between these dates
-
-        To be "interesting":
-        - A "completed" order where the completion date lies within the date range
-        - A "pending" order where the target date lies within the date range
-        - TODO: An "overdue" order where the target date is in the past
-        """
-        date_fmt = '%Y-%m-%d'  # ISO format date string
-
-        # Ensure that both dates are valid
-        try:
-            min_date = datetime.strptime(str(min_date), date_fmt).date()
-            max_date = datetime.strptime(str(max_date), date_fmt).date()
-        except (ValueError, TypeError):
-            # Date processing error, return queryset unchanged
-            return queryset
-
-        # Construct a queryset for "completed" orders within the range
-        completed = (
-            Q(status__in=SalesOrderStatusGroups.COMPLETE)
-            & Q(shipment_date__gte=min_date)
-            & Q(shipment_date__lte=max_date)
-        )
-
-        # Construct a queryset for "pending" orders within the range
-        pending = (
-            Q(status__in=SalesOrderStatusGroups.OPEN)
-            & ~Q(target_date=None)
-            & Q(target_date__gte=min_date)
-            & Q(target_date__lte=max_date)
-        )
-
-        # TODO: Construct a queryset for "overdue" orders within the range
-
-        queryset = queryset.filter(completed | pending)
-
-        return queryset
-
     def __str__(self):
         """Render a string representation of this SalesOrder."""
         return f'{self.reference} - {self.customer.name if self.customer else _("deleted")}'
@@ -2363,13 +2274,6 @@ class ReturnOrder(TotalPriceMixin, Order):
         help_text=_('Customer order reference code'),
     )
 
-    issue_date = models.DateField(
-        blank=True,
-        null=True,
-        verbose_name=_('Issue Date'),
-        help_text=_('Date order was issued'),
-    )
-
     complete_date = models.DateField(
         blank=True,
         null=True,
diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py
index 5e105f4f5a..36f689ae89 100644
--- a/src/backend/InvenTree/order/serializers.py
+++ b/src/backend/InvenTree/order/serializers.py
@@ -193,6 +193,7 @@ class AbstractOrderSerializer(DataImportExportSerializerMixin, serializers.Seria
             'pk',
             'created_by',
             'creation_date',
+            'issue_date',
             'start_date',
             'target_date',
             'description',
@@ -322,7 +323,6 @@ class PurchaseOrderSerializer(
         model = order.models.PurchaseOrder
 
         fields = AbstractOrderSerializer.order_fields([
-            'issue_date',
             'complete_date',
             'supplier',
             'supplier_detail',
@@ -1880,7 +1880,6 @@ class ReturnOrderSerializer(
         model = order.models.ReturnOrder
 
         fields = AbstractOrderSerializer.order_fields([
-            'issue_date',
             'complete_date',
             'customer',
             'customer_detail',
diff --git a/src/frontend/package.json b/src/frontend/package.json
index a04ec2a858..2a6f5e5f24 100644
--- a/src/frontend/package.json
+++ b/src/frontend/package.json
@@ -24,6 +24,10 @@
         "@fortawesome/free-regular-svg-icons": "^6.6.0",
         "@fortawesome/free-solid-svg-icons": "^6.6.0",
         "@fortawesome/react-fontawesome": "^0.2.2",
+        "@fullcalendar/core": "^6.1.15",
+        "@fullcalendar/daygrid": "^6.1.15",
+        "@fullcalendar/interaction": "^6.1.15",
+        "@fullcalendar/react": "^6.1.15",
         "@lingui/core": "^4.11.4",
         "@lingui/react": "^4.11.4",
         "@mantine/carousel": "^7.16.0",
diff --git a/src/frontend/src/components/buttons/SegmentedIconControl.tsx b/src/frontend/src/components/buttons/SegmentedIconControl.tsx
new file mode 100644
index 0000000000..9cdbfc9132
--- /dev/null
+++ b/src/frontend/src/components/buttons/SegmentedIconControl.tsx
@@ -0,0 +1,51 @@
+import {
+  ActionIcon,
+  type MantineColor,
+  type MantineSize,
+  SegmentedControl,
+  Tooltip
+} from '@mantine/core';
+import type { ReactNode } from 'react';
+
+export type SegmentedIconControlItem = {
+  label: string;
+  value: string;
+  icon: ReactNode;
+};
+
+export default function SegmentedIconControl({
+  data,
+  value,
+  size = 'sm',
+  color,
+  onChange
+}: {
+  data: SegmentedIconControlItem[];
+  value: string;
+  size?: MantineSize;
+  color?: MantineColor;
+  onChange: (value: string) => void;
+}) {
+  return (
+    <SegmentedControl
+      value={value}
+      onChange={onChange}
+      data={data.map((item) => ({
+        value: item.value,
+        label: (
+          <Tooltip label={item.label}>
+            <ActionIcon
+              variant='transparent'
+              color={color}
+              size={size}
+              aria-label={`segmented-icon-control-${item.value}`}
+              onClick={() => onChange(item.value)}
+            >
+              {item.icon}
+            </ActionIcon>
+          </Tooltip>
+        )
+      }))}
+    />
+  );
+}
diff --git a/src/frontend/src/components/calendar/Calendar.tsx b/src/frontend/src/components/calendar/Calendar.tsx
new file mode 100644
index 0000000000..9d0e8b3909
--- /dev/null
+++ b/src/frontend/src/components/calendar/Calendar.tsx
@@ -0,0 +1,190 @@
+import type { CalendarOptions, DatesSetArg } from '@fullcalendar/core';
+import allLocales from '@fullcalendar/core/locales-all';
+import dayGridPlugin from '@fullcalendar/daygrid';
+import interactionPlugin from '@fullcalendar/interaction';
+import FullCalendar from '@fullcalendar/react';
+
+import { t } from '@lingui/macro';
+import {
+  ActionIcon,
+  Box,
+  Button,
+  Group,
+  Indicator,
+  LoadingOverlay,
+  Popover,
+  Stack,
+  Tooltip
+} from '@mantine/core';
+import { type DateValue, MonthPicker } from '@mantine/dates';
+import {
+  IconCalendarMonth,
+  IconChevronLeft,
+  IconChevronRight,
+  IconFilter
+} from '@tabler/icons-react';
+import { useCallback, useState } from 'react';
+import type { CalendarState } from '../../hooks/UseCalendar';
+import { useLocalState } from '../../states/LocalState';
+import { DownloadAction } from '../../tables/DownloadAction';
+import type { TableFilter } from '../../tables/Filter';
+import { FilterSelectDrawer } from '../../tables/FilterSelectDrawer';
+import { TableSearchInput } from '../../tables/Search';
+import { Boundary } from '../Boundary';
+import { ActionButton } from '../buttons/ActionButton';
+import { StylishText } from '../items/StylishText';
+
+export interface InvenTreeCalendarProps extends CalendarOptions {
+  downloadData?: (fileFormat: string) => void;
+  enableDownload?: boolean;
+  enableFilters?: boolean;
+  enableSearch?: boolean;
+  filters?: TableFilter[];
+  isLoading?: boolean;
+  state: CalendarState;
+}
+
+export default function Calendar({
+  downloadData,
+  enableDownload,
+  enableFilters = false,
+  enableSearch,
+  isLoading,
+  filters,
+  state,
+  ...calendarProps
+}: InvenTreeCalendarProps) {
+  const [monthSelectOpened, setMonthSelectOpened] = useState<boolean>(false);
+
+  const [filtersVisible, setFiltersVisible] = useState<boolean>(false);
+
+  const [locale] = useLocalState((s) => [s.language]);
+
+  const selectMonth = useCallback(
+    (date: DateValue) => {
+      state.selectMonth(date);
+      setMonthSelectOpened(false);
+    },
+    [state.selectMonth]
+  );
+
+  // Callback when the calendar date range is adjusted
+  const datesSet = useCallback(
+    (dateInfo: DatesSetArg) => {
+      if (state.ref?.current) {
+        const api = state.ref.current.getApi();
+
+        // Update calendar state
+        state.setMonthName(api.view.title);
+        state.setStartDate(dateInfo.start);
+        state.setEndDate(dateInfo.end);
+      }
+
+      // Pass the dates set to the parent component
+      calendarProps.datesSet?.(dateInfo);
+    },
+    [calendarProps.datesSet, state.ref, state.setMonthName]
+  );
+
+  return (
+    <>
+      {enableFilters && filters && (filters?.length ?? 0) > 0 && (
+        <Boundary label={`InvenTreeCalendarFilterDrawer-${state.name}`}>
+          <FilterSelectDrawer
+            title={t`Calendar Filters`}
+            availableFilters={filters}
+            filterSet={state.filterSet}
+            opened={filtersVisible}
+            onClose={() => setFiltersVisible(false)}
+          />
+        </Boundary>
+      )}
+      <Stack gap='xs'>
+        <Group justify='space-between' gap='xs'>
+          <Group gap={0} justify='left'>
+            <ActionButton
+              icon={<IconChevronLeft />}
+              onClick={state.prevMonth}
+              tooltipAlignment='top'
+              tooltip={t`Previous month`}
+            />
+            <Popover
+              opened={monthSelectOpened}
+              onClose={() => setMonthSelectOpened(false)}
+              position='bottom-start'
+              shadow='md'
+            >
+              <Popover.Target>
+                <Tooltip label={t`Select month`} position='top'>
+                  <Button
+                    m={0}
+                    variant='transparent'
+                    aria-label='calendar-select-month'
+                    onClick={() => {
+                      setMonthSelectOpened(!monthSelectOpened);
+                    }}
+                  >
+                    <IconCalendarMonth />
+                  </Button>
+                </Tooltip>
+              </Popover.Target>
+              <Popover.Dropdown>
+                <MonthPicker onChange={selectMonth} />
+              </Popover.Dropdown>
+            </Popover>
+            <ActionButton
+              icon={<IconChevronRight />}
+              onClick={state.nextMonth}
+              tooltipAlignment='top'
+              tooltip={t`Next month`}
+            />
+            <StylishText size='lg'>{state.monthName}</StylishText>
+          </Group>
+          <Group justify='right' gap='xs' wrap='nowrap'>
+            {enableSearch && (
+              <TableSearchInput searchCallback={state.setSearchTerm} />
+            )}
+            {enableFilters && filters && filters.length > 0 && (
+              <Indicator
+                size='xs'
+                label={state.filterSet.activeFilters?.length ?? 0}
+                disabled={state.filterSet.activeFilters?.length == 0}
+              >
+                <ActionIcon
+                  variant='transparent'
+                  aria-label='calendar-select-filters'
+                >
+                  <Tooltip label={t`Calendar Filters`}>
+                    <IconFilter
+                      onClick={() => setFiltersVisible(!filtersVisible)}
+                    />
+                  </Tooltip>
+                </ActionIcon>
+              </Indicator>
+            )}
+            {enableDownload && (
+              <DownloadAction
+                key='download-action'
+                downloadCallback={downloadData}
+              />
+            )}
+          </Group>
+        </Group>
+        <Box pos='relative'>
+          <LoadingOverlay visible={state.query.isFetching} />
+          <FullCalendar
+            ref={state.ref}
+            plugins={[dayGridPlugin, interactionPlugin]}
+            initialView='dayGridMonth'
+            locales={allLocales}
+            locale={locale}
+            headerToolbar={false}
+            footerToolbar={false}
+            {...calendarProps}
+            datesSet={datesSet}
+          />
+        </Box>
+      </Stack>
+    </>
+  );
+}
diff --git a/src/frontend/src/components/calendar/OrderCalendar.tsx b/src/frontend/src/components/calendar/OrderCalendar.tsx
new file mode 100644
index 0000000000..b00134ddcc
--- /dev/null
+++ b/src/frontend/src/components/calendar/OrderCalendar.tsx
@@ -0,0 +1,209 @@
+import type {
+  EventChangeArg,
+  EventClickArg,
+  EventContentArg
+} from '@fullcalendar/core';
+import { t } from '@lingui/macro';
+import { ActionIcon, Group, Text } from '@mantine/core';
+import { hideNotification, showNotification } from '@mantine/notifications';
+import {
+  IconCalendarExclamation,
+  IconCircleCheck,
+  IconExclamationCircle
+} from '@tabler/icons-react';
+import dayjs from 'dayjs';
+import { useCallback, useMemo } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { api } from '../../App';
+import type { ModelType } from '../../enums/ModelType';
+import type { UserRoles } from '../../enums/Roles';
+import { navigateToLink } from '../../functions/navigation';
+import { getDetailUrl } from '../../functions/urls';
+import useCalendar from '../../hooks/UseCalendar';
+import { apiUrl } from '../../states/ApiState';
+import { useUserState } from '../../states/UserState';
+import {
+  AssignedToMeFilter,
+  HasProjectCodeFilter,
+  OrderStatusFilter,
+  ProjectCodeFilter,
+  ResponsibleFilter,
+  type TableFilter
+} from '../../tables/Filter';
+import { ModelInformationDict } from '../render/ModelType';
+import { StatusRenderer } from '../render/StatusRenderer';
+import Calendar from './Calendar';
+
+/**
+ * A generic calendar component for displaying orders
+ * This can be used for the following order types:
+ * - BuildOrder
+ * - PurchaseOrder
+ * - SalesOrder
+ * - ReturnOrder
+ */
+
+export default function OrderCalendar({
+  model,
+  role,
+  params,
+  filters
+}: {
+  model: ModelType;
+  role: UserRoles;
+  params: Record<string, any>;
+  filters?: TableFilter[];
+}) {
+  const navigate = useNavigate();
+  const user = useUserState();
+
+  // These filters apply to all order types
+  const orderFilters: TableFilter[] = useMemo(() => {
+    return [
+      OrderStatusFilter({ model: model }),
+      AssignedToMeFilter(),
+      ProjectCodeFilter(),
+      HasProjectCodeFilter(),
+      ResponsibleFilter()
+    ];
+  }, [model]);
+
+  // Complete set of available filters
+  const calendarFilters: TableFilter[] = useMemo(() => {
+    return [...orderFilters, ...(filters ?? [])];
+  }, [orderFilters, filters]);
+
+  const modelInfo = useMemo(() => {
+    return ModelInformationDict[model];
+  }, [model]);
+
+  const canEdit = useMemo(() => {
+    return user.hasChangeRole(role);
+  }, [user, role]);
+
+  const calendarState = useCalendar({
+    endpoint: modelInfo.api_endpoint,
+    name: model.toString(),
+    queryParams: params
+  });
+
+  // Build the events
+  const events = useMemo(() => {
+    const today = dayjs().format('YYYY-MM-DD');
+
+    return (
+      calendarState.data?.map((order: any) => {
+        const start: string =
+          order.start_date || order.issue_date || order.creation_date || today;
+        const end: string = order.target_date || start;
+
+        return {
+          id: order.pk,
+          title: order.reference,
+          description: order.description,
+          start: start,
+          end: end,
+          startEditable: canEdit,
+          durationEditable: canEdit
+        };
+      }) ?? []
+    );
+  }, [calendarState.data, canEdit]);
+
+  // Callback when Order is edited
+  const onEditOrder = (info: EventChangeArg) => {
+    const orderId = info.event.id;
+    const patch: Record<string, string> = {};
+
+    if (info.event.start && info.event.start != info.oldEvent.start) {
+      patch.start_date = dayjs(info.event.start).format('YYYY-MM-DD');
+    }
+
+    if (info.event.end && info.event.end != info.oldEvent.end) {
+      patch.target_date = dayjs(info.event.end).format('YYYY-MM-DD');
+    }
+
+    if (!!patch) {
+      api
+        .patch(apiUrl(modelInfo.api_endpoint, orderId), patch)
+        .then(() => {
+          hideNotification('calendar-edit-result');
+          showNotification({
+            id: 'calendar-edit-result',
+            message: t`Order Updated`,
+            color: 'green',
+            icon: <IconCircleCheck />
+          });
+        })
+        .catch(() => {
+          info.revert();
+          hideNotification('calendar-edit-result');
+          showNotification({
+            id: 'calendar-edit-result',
+            message: t`Error updating order`,
+            color: 'red',
+            icon: <IconExclamationCircle />
+          });
+        });
+    }
+  };
+
+  // Callback when PurchaseOrder is clicked
+  const onClickOrder = (info: EventClickArg) => {
+    if (!!info.event.id) {
+      navigateToLink(
+        getDetailUrl(model, info.event.id),
+        navigate,
+        info.jsEvent
+      );
+    }
+  };
+
+  const renderOrder = useCallback(
+    (event: EventContentArg) => {
+      const order = calendarState.data?.find(
+        (order: any) => order.pk.toString() == event.event.id.toString()
+      );
+
+      if (!order) {
+        // Fallback to the event title if no order is found
+        return event.event.title;
+      }
+
+      return (
+        <Group gap='xs' wrap='nowrap'>
+          {order.overdue && (
+            <ActionIcon
+              color='orange-7'
+              variant='transparent'
+              title={t`Overdue`}
+            >
+              <IconCalendarExclamation />
+            </ActionIcon>
+          )}
+          <Text size='sm' fw={700}>
+            {order.reference}
+          </Text>
+          <Text size='xs'>{order.description ?? order.title}</Text>
+          <StatusRenderer status={order.status} type={model} />
+        </Group>
+      );
+    },
+    [calendarState.data, model]
+  );
+
+  return (
+    <Calendar
+      enableDownload
+      enableFilters
+      enableSearch
+      events={events}
+      state={calendarState}
+      filters={calendarFilters}
+      editable={true}
+      eventContent={renderOrder}
+      eventClick={onClickOrder}
+      eventChange={onEditOrder}
+    />
+  );
+}
diff --git a/src/frontend/src/components/items/ActionDropdown.tsx b/src/frontend/src/components/items/ActionDropdown.tsx
index a18f4ec1d2..e7183974b8 100644
--- a/src/frontend/src/components/items/ActionDropdown.tsx
+++ b/src/frontend/src/components/items/ActionDropdown.tsx
@@ -1,6 +1,7 @@
 import { t } from '@lingui/macro';
 import {
   Button,
+  type FloatingPosition,
   Indicator,
   type IndicatorProps,
   Menu,
@@ -43,6 +44,7 @@ export type ActionDropdownItem = {
 export function ActionDropdown({
   icon,
   tooltip,
+  tooltipPosition,
   actions,
   disabled = false,
   hidden = false,
@@ -50,6 +52,7 @@ export function ActionDropdown({
 }: {
   icon: ReactNode;
   tooltip: string;
+  tooltipPosition?: FloatingPosition;
   actions: ActionDropdownItem[];
   disabled?: boolean;
   hidden?: boolean;
@@ -71,7 +74,11 @@ export function ActionDropdown({
     <Menu position='bottom-end' key={menuName}>
       <Indicator disabled={!indicatorProps} {...indicatorProps?.indicator}>
         <Menu.Target>
-          <Tooltip label={tooltip} hidden={!tooltip} position='bottom'>
+          <Tooltip
+            label={tooltip}
+            hidden={!tooltip}
+            position={tooltipPosition ?? 'bottom'}
+          >
             <Button
               variant={noindicator ? 'transparent' : 'light'}
               disabled={disabled}
diff --git a/src/frontend/src/components/panels/Panel.tsx b/src/frontend/src/components/panels/Panel.tsx
index 824200ef0b..a1e407cd5f 100644
--- a/src/frontend/src/components/panels/Panel.tsx
+++ b/src/frontend/src/components/panels/Panel.tsx
@@ -6,6 +6,7 @@ import type { ReactNode } from 'react';
 export type PanelType = {
   name: string;
   label: string;
+  controls?: ReactNode;
   icon?: ReactNode;
   content: ReactNode;
   hidden?: boolean;
diff --git a/src/frontend/src/components/panels/PanelGroup.tsx b/src/frontend/src/components/panels/PanelGroup.tsx
index e7dd751cf3..dcdc60594a 100644
--- a/src/frontend/src/components/panels/PanelGroup.tsx
+++ b/src/frontend/src/components/panels/PanelGroup.tsx
@@ -227,7 +227,14 @@ function BasePanelGroup({
                   <Stack gap='md'>
                     {panel.showHeadline !== false && (
                       <>
-                        <StylishText size='xl'>{panel.label}</StylishText>
+                        <Group justify='space-between'>
+                          <StylishText size='xl'>{panel.label}</StylishText>
+                          {panel.controls && (
+                            <Group justify='right' wrap='nowrap'>
+                              {panel.controls}
+                            </Group>
+                          )}
+                        </Group>
                         <Divider />
                       </>
                     )}
diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx
index 8dbf466dbd..a0a1018d4e 100644
--- a/src/frontend/src/forms/StockForms.tsx
+++ b/src/frontend/src/forms/StockForms.tsx
@@ -115,7 +115,9 @@ export function useStockFields({
 
           if (expiry_days && expiry_days > 0) {
             // Adjust the expiry date based on the part default expiry
-            setExpiryDate(dayjs().add(expiry_days, 'days').toISOString());
+            setExpiryDate(
+              dayjs().add(expiry_days, 'days').format('YYYY-MM-DD')
+            );
           }
         }
       },
diff --git a/src/frontend/src/hooks/UseCalendar.tsx b/src/frontend/src/hooks/UseCalendar.tsx
new file mode 100644
index 0000000000..dfa163616e
--- /dev/null
+++ b/src/frontend/src/hooks/UseCalendar.tsx
@@ -0,0 +1,166 @@
+import type FullCalendar from '@fullcalendar/react';
+import type { DateValue } from '@mantine/dates';
+import { type UseQueryResult, useQuery } from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import { useCallback, useMemo, useRef, useState } from 'react';
+import { api } from '../App';
+import type { ApiEndpoints } from '../enums/ApiEndpoints';
+import { showApiErrorMessage } from '../functions/notifications';
+import { apiUrl } from '../states/ApiState';
+import { type FilterSetState, useFilterSet } from './UseFilterSet';
+
+/*
+ * Type definition for representing the state of a calendar:
+ *
+ * ref: A reference to the FullCalendar component
+ * filterSet: The current filter set state
+ * monthName: The name of the current month (e.g. "January 2022")
+ * setMonthName: A function to set the month name
+ * searchTerm: The current search term for the calendar
+ * setSearchTerm: A function to set the search term
+ * startDate: The start date of the current date range
+ * setStartDate: A function to set the start date
+ * endDate: The end date of the current date range
+ * setEndDate: A function to set the end date
+ * nextMonth: A function to navigate to the next month
+ * prevMonth: A function to navigate to the previous month
+ * currentMonth: A function to navigate to the current month
+ * selectMonth: A function to select a specific month
+ */
+export type CalendarState = {
+  name: string;
+  ref: React.RefObject<FullCalendar>;
+  filterSet: FilterSetState;
+  monthName: string;
+  setMonthName: (name: string) => void;
+  searchTerm: string;
+  startDate: Date | null;
+  setStartDate: (date: Date | null) => void;
+  endDate: Date | null;
+  setEndDate: (date: Date | null) => void;
+  setSearchTerm: (term: string) => void;
+  nextMonth: () => void;
+  prevMonth: () => void;
+  currentMonth: () => void;
+  selectMonth: (date: DateValue) => void;
+  query: UseQueryResult;
+  data: any;
+};
+
+export default function useCalendar({
+  name,
+  endpoint,
+  queryParams
+}: {
+  name: string;
+  endpoint: ApiEndpoints;
+  queryParams?: any;
+}): CalendarState {
+  const ref = useRef<FullCalendar | null>(null);
+
+  const filterSet = useFilterSet(`calendar-${name}`);
+
+  const [searchTerm, setSearchTerm] = useState<string>('');
+
+  const [monthName, setMonthName] = useState<string>('');
+
+  const [startDate, setStartDate] = useState<Date | null>(null);
+
+  const [endDate, setEndDate] = useState<Date | null>(null);
+
+  // Generate a set of API query filters
+  const queryFilters = useMemo(() => {
+    // Expand date range by one month, to ensure we capture all events
+
+    let params = {
+      ...(queryParams || {})
+    };
+
+    if (filterSet.activeFilters) {
+      filterSet.activeFilters.forEach((filter) => {
+        params[filter.name] = filter.value;
+      });
+    }
+
+    params = {
+      ...params,
+      min_date: startDate
+        ? dayjs(startDate).subtract(1, 'month').format('YYYY-MM-DD')
+        : null,
+      max_date: endDate
+        ? dayjs(endDate).add(1, 'month').format('YYYY-MM-DD')
+        : null,
+      search: searchTerm
+    };
+
+    return params;
+  }, [startDate, endDate, searchTerm, filterSet.activeFilters, queryParams]);
+
+  const query = useQuery({
+    enabled: !!startDate && !!endDate,
+    queryKey: ['calendar', name, endpoint, queryFilters],
+    queryFn: async () => {
+      // Fetch data from the API
+      return api
+        .get(apiUrl(endpoint), {
+          params: queryFilters
+        })
+        .then((response) => {
+          return response.data ?? [];
+        })
+        .catch((error) => {
+          showApiErrorMessage({
+            error: error,
+            title: 'Error fetching calendar data'
+          });
+        });
+    }
+  });
+
+  // Navigate to the previous month
+  const prevMonth = useCallback(() => {
+    ref.current?.getApi().prev();
+  }, [ref]);
+
+  // Navigate to the next month
+  const nextMonth = useCallback(() => {
+    ref.current?.getApi().next();
+  }, [ref]);
+
+  // Navigate to the current month
+  const currentMonth = useCallback(() => {
+    ref.current?.getApi().today();
+  }, [ref]);
+
+  // Callback to select a specific month from a picker
+  const selectMonth = useCallback(
+    (date: DateValue) => {
+      if (date && ref?.current) {
+        const api = ref.current.getApi();
+
+        api.gotoDate(date);
+      }
+    },
+    [ref]
+  );
+
+  return {
+    name,
+    filterSet,
+    ref,
+    monthName,
+    setMonthName,
+    searchTerm,
+    setSearchTerm,
+    nextMonth,
+    prevMonth,
+    currentMonth,
+    selectMonth,
+    startDate,
+    setStartDate,
+    endDate,
+    setEndDate,
+    query: query,
+    data: query.data
+  };
+}
diff --git a/src/frontend/src/hooks/UseFilterSet.tsx b/src/frontend/src/hooks/UseFilterSet.tsx
new file mode 100644
index 0000000000..41a6d9b295
--- /dev/null
+++ b/src/frontend/src/hooks/UseFilterSet.tsx
@@ -0,0 +1,40 @@
+import { useLocalStorage } from '@mantine/hooks';
+import { useCallback } from 'react';
+import type { TableFilter } from '../tables/Filter';
+
+/*
+ * Type definition for representing the state of a group of filters.
+ * These may be applied to a data view (e.g. table, calendar) to filter the displayed data.
+ *
+ * filterKey: A unique key for the filter set
+ * activeFilters: An array of active filters
+ * setActiveFilters: A function to set the active filters
+ * clearActiveFilters: A function to clear all active filters
+ */
+export type FilterSetState = {
+  filterKey: string;
+  activeFilters: TableFilter[];
+  setActiveFilters: (filters: TableFilter[]) => void;
+  clearActiveFilters: () => void;
+};
+
+export function useFilterSet(filterKey: string): FilterSetState {
+  // Array of active filters (saved to local storage)
+  const [activeFilters, setActiveFilters] = useLocalStorage<TableFilter[]>({
+    key: `inventree-filterset-${filterKey}`,
+    defaultValue: [],
+    getInitialValueInEffect: false
+  });
+
+  // Callback to clear all active filters from the table
+  const clearActiveFilters = useCallback(() => {
+    setActiveFilters([]);
+  }, []);
+
+  return {
+    filterKey,
+    activeFilters,
+    setActiveFilters,
+    clearActiveFilters
+  };
+}
diff --git a/src/frontend/src/hooks/UseTable.tsx b/src/frontend/src/hooks/UseTable.tsx
index eaa43a08ce..4099014040 100644
--- a/src/frontend/src/hooks/UseTable.tsx
+++ b/src/frontend/src/hooks/UseTable.tsx
@@ -2,7 +2,7 @@ import { randomId, useLocalStorage } from '@mantine/hooks';
 import { useCallback, useMemo, useState } from 'react';
 import { type SetURLSearchParams, useSearchParams } from 'react-router-dom';
 
-import type { TableFilter } from '../tables/Filter';
+import { type FilterSetState, useFilterSet } from './UseFilterSet';
 
 /*
  * Type definition for representing the state of a table:
@@ -11,9 +11,7 @@ import type { TableFilter } from '../tables/Filter';
  * refreshTable: A callback function to externally refresh the table.
  * isLoading: A boolean flag to indicate if the table is currently loading data
  * setIsLoading: A function to set the isLoading flag
- * activeFilters: An array of active filters (saved to local storage)
- * setActiveFilters: A function to set the active filters
- * clearActiveFilters: A function to clear all active filters
+ * filterSet: A group of active filters
  * queryFilters: A map of query filters (e.g. ?active=true&overdue=false) passed in the URL
  * setQueryFilters: A function to set the query filters
  * clearQueryFilters: A function to clear all query filters
@@ -45,9 +43,7 @@ export type TableState = {
   refreshTable: () => void;
   isLoading: boolean;
   setIsLoading: (value: boolean) => void;
-  activeFilters: TableFilter[];
-  setActiveFilters: (filters: TableFilter[]) => void;
-  clearActiveFilters: () => void;
+  filterSet: FilterSetState;
   queryFilters: URLSearchParams;
   setQueryFilters: SetURLSearchParams;
   clearQueryFilters: () => void;
@@ -101,17 +97,7 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState {
     setTableKey(generateTableName());
   }, [generateTableName]);
 
-  // Array of active filters (saved to local storage)
-  const [activeFilters, setActiveFilters] = useLocalStorage<TableFilter[]>({
-    key: `inventree-table-filters-${tableName}`,
-    defaultValue: [],
-    getInitialValueInEffect: false
-  });
-
-  // Callback to clear all active filters from the table
-  const clearActiveFilters = useCallback(() => {
-    setActiveFilters([]);
-  }, []);
+  const filterSet: FilterSetState = useFilterSet(`table-${tableName}`);
 
   // Array of expanded records
   const [expandedRecords, setExpandedRecords] = useState<any[]>([]);
@@ -194,9 +180,7 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState {
     refreshTable,
     isLoading,
     setIsLoading,
-    activeFilters,
-    setActiveFilters,
-    clearActiveFilters,
+    filterSet,
     queryFilters,
     setQueryFilters,
     clearQueryFilters,
diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx
index 310e190632..02c013c757 100644
--- a/src/frontend/src/pages/build/BuildDetail.tsx
+++ b/src/frontend/src/pages/build/BuildDetail.tsx
@@ -181,6 +181,40 @@ export default function BuildDetail() {
         badge: 'owner',
         hidden: !build.responsible
       },
+      {
+        type: 'text',
+        name: 'project_code_label',
+        label: t`Project Code`,
+        icon: 'reference',
+        copy: true,
+        hidden: !build.project_code
+      },
+      {
+        type: 'link',
+        name: 'take_from',
+        icon: 'location',
+        model: ModelType.stocklocation,
+        label: t`Source Location`,
+        backup_value: t`Any location`
+      },
+      {
+        type: 'link',
+        name: 'destination',
+        icon: 'location',
+        model: ModelType.stocklocation,
+        label: t`Destination Location`,
+        hidden: !build.destination
+      },
+      {
+        type: 'text',
+        name: 'batch',
+        label: t`Batch Code`,
+        hidden: !build.batch,
+        copy: true
+      }
+    ];
+
+    const br: DetailsField[] = [
       {
         type: 'date',
         name: 'creation_date',
@@ -212,40 +246,6 @@ export default function BuildDetail() {
         icon: 'calendar',
         copy: true,
         hidden: !build.completion_date
-      },
-      {
-        type: 'text',
-        name: 'project_code_label',
-        label: t`Project Code`,
-        icon: 'reference',
-        copy: true,
-        hidden: !build.project_code
-      }
-    ];
-
-    const br: DetailsField[] = [
-      {
-        type: 'link',
-        name: 'take_from',
-        icon: 'location',
-        model: ModelType.stocklocation,
-        label: t`Source Location`,
-        backup_value: t`Any location`
-      },
-      {
-        type: 'link',
-        name: 'destination',
-        icon: 'location',
-        model: ModelType.stocklocation,
-        label: t`Destination Location`,
-        hidden: !build.destination
-      },
-      {
-        type: 'text',
-        name: 'batch',
-        label: t`Batch Code`,
-        hidden: !build.batch,
-        copy: true
       }
     ];
 
diff --git a/src/frontend/src/pages/build/BuildIndex.tsx b/src/frontend/src/pages/build/BuildIndex.tsx
index c2be829f62..e67f1958a3 100644
--- a/src/frontend/src/pages/build/BuildIndex.tsx
+++ b/src/frontend/src/pages/build/BuildIndex.tsx
@@ -1,35 +1,89 @@
 import { t } from '@lingui/macro';
 import { Stack } from '@mantine/core';
-import { IconTools } from '@tabler/icons-react';
+import { IconCalendar, IconTable, IconTools } from '@tabler/icons-react';
 import { useMemo } from 'react';
 
+import { useLocalStorage } from '@mantine/hooks';
+import SegmentedIconControl from '../../components/buttons/SegmentedIconControl';
+import OrderCalendar from '../../components/calendar/OrderCalendar';
 import PermissionDenied from '../../components/errors/PermissionDenied';
 import { PageDetail } from '../../components/nav/PageDetail';
+import type { PanelType } from '../../components/panels/Panel';
 import { PanelGroup } from '../../components/panels/PanelGroup';
+import { ModelType } from '../../enums/ModelType';
 import { UserRoles } from '../../enums/Roles';
 import { useUserState } from '../../states/UserState';
+import { PartCategoryFilter, type TableFilter } from '../../tables/Filter';
 import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
 
+function BuildOrderCalendar() {
+  const calendarFilters: TableFilter[] = useMemo(() => {
+    return [PartCategoryFilter()];
+  }, []);
+
+  return (
+    <OrderCalendar
+      model={ModelType.build}
+      role={UserRoles.build}
+      params={{ outstanding: true }}
+      filters={calendarFilters}
+    />
+  );
+}
+
+function BuildOverview({
+  view
+}: {
+  view: string;
+}) {
+  switch (view) {
+    case 'calendar':
+      return <BuildOrderCalendar />;
+    case 'table':
+    default:
+      return <BuildOrderTable />;
+  }
+}
+
 /**
  * Build Order index page
  */
 export default function BuildIndex() {
   const user = useUserState();
 
-  if (!user.isLoggedIn() || !user.hasViewRole(UserRoles.build)) {
-    return <PermissionDenied />;
-  }
+  const [buildOrderView, setBuildOrderView] = useLocalStorage<string>({
+    key: 'buildOrderView',
+    defaultValue: 'table'
+  });
 
-  const panels = useMemo(() => {
+  const panels: PanelType[] = useMemo(() => {
     return [
       {
         name: 'buildorders',
         label: t`Build Orders`,
-        content: <BuildOrderTable />,
-        icon: <IconTools />
+        content: <BuildOverview view={buildOrderView} />,
+        icon: <IconTools />,
+        controls: (
+          <SegmentedIconControl
+            value={buildOrderView}
+            onChange={setBuildOrderView}
+            data={[
+              { value: 'table', label: t`Table View`, icon: <IconTable /> },
+              {
+                value: 'calendar',
+                label: t`Calendar View`,
+                icon: <IconCalendar />
+              }
+            ]}
+          />
+        )
       }
     ];
-  }, []);
+  }, [buildOrderView, setBuildOrderView]);
+
+  if (!user.isLoggedIn() || !user.hasViewRole(UserRoles.build)) {
+    return <PermissionDenied />;
+  }
 
   return (
     <Stack>
diff --git a/src/frontend/src/pages/part/PartSchedulingDetail.tsx b/src/frontend/src/pages/part/PartSchedulingDetail.tsx
index 1ec87409d4..26e65bfac9 100644
--- a/src/frontend/src/pages/part/PartSchedulingDetail.tsx
+++ b/src/frontend/src/pages/part/PartSchedulingDetail.tsx
@@ -12,6 +12,7 @@ import {
 import { type ReactNode, useMemo } from 'react';
 import { useNavigate } from 'react-router-dom';
 
+import dayjs from 'dayjs';
 import { formatDate } from '../../defaults/formatters';
 import { ApiEndpoints } from '../../enums/ApiEndpoints';
 import { navigateToLink } from '../../functions/navigation';
@@ -32,7 +33,7 @@ function ChartTooltip({ label, payload }: Readonly<ChartTooltipProps>) {
   }
 
   if (label && typeof label == 'number') {
-    label = formatDate(new Date(label).toISOString());
+    label = formatDate(dayjs().format('YYYY-MM-DD'));
   }
 
   const scheduled = payload.find((item) => item.name == 'scheduled');
@@ -154,7 +155,6 @@ export default function PartSchedulingDetail({
     // Construct initial chart entry (for today)
     const entries: any[] = [
       {
-        // date: formatDate(today.toISOString()),
         date: today.valueOf(),
         delta: 0,
         scheduled: stock,
@@ -282,7 +282,7 @@ export default function PartSchedulingDetail({
               scale: 'time',
               type: 'number',
               tickFormatter: (value: number) => {
-                return formatDate(new Date(value).toISOString());
+                return formatDate(dayjs().format('YYYY-MM-DD'));
               }
             }}
             series={[
diff --git a/src/frontend/src/pages/part/PartStocktakeDetail.tsx b/src/frontend/src/pages/part/PartStocktakeDetail.tsx
index 78613a448d..51a47e4ec6 100644
--- a/src/frontend/src/pages/part/PartStocktakeDetail.tsx
+++ b/src/frontend/src/pages/part/PartStocktakeDetail.tsx
@@ -10,6 +10,7 @@ import {
 } from '@mantine/core';
 import { useCallback, useMemo, useState } from 'react';
 
+import dayjs from 'dayjs';
 import { AddItemButton } from '../../components/buttons/AddItemButton';
 import { formatDate, formatPriceRange } from '../../defaults/formatters';
 import { ApiEndpoints } from '../../enums/ApiEndpoints';
@@ -36,7 +37,7 @@ import { RowDeleteAction, RowEditAction } from '../../tables/RowActions';
 function ChartTooltip({ label, payload }: Readonly<ChartTooltipProps>) {
   const formattedLabel: string = useMemo(() => {
     if (label && typeof label === 'number') {
-      return formatDate(new Date(label).toISOString()) ?? label;
+      return formatDate(dayjs().format('YYYY-MM-DD')) ?? label;
     } else if (!!label) {
       return label.toString();
     } else {
@@ -253,7 +254,7 @@ export default function PartStocktakeDetail({
               type: 'number',
               domain: chartLimits,
               tickFormatter: (value: number) => {
-                return formatDate(new Date(value).toISOString());
+                return formatDate(dayjs().format('YYYY-MM-DD'));
               }
             }}
             series={[
diff --git a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx
index b540664a3c..4f10bf577d 100644
--- a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx
+++ b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx
@@ -4,14 +4,20 @@ import {
   IconBuildingFactory2,
   IconBuildingStore,
   IconBuildingWarehouse,
+  IconCalendar,
   IconPackageExport,
-  IconShoppingCart
+  IconShoppingCart,
+  IconTable
 } from '@tabler/icons-react';
 import { useMemo } from 'react';
 
+import { useLocalStorage } from '@mantine/hooks';
+import SegmentedIconControl from '../../components/buttons/SegmentedIconControl';
+import OrderCalendar from '../../components/calendar/OrderCalendar';
 import PermissionDenied from '../../components/errors/PermissionDenied';
 import { PageDetail } from '../../components/nav/PageDetail';
 import { PanelGroup } from '../../components/panels/PanelGroup';
+import { ModelType } from '../../enums/ModelType';
 import { UserRoles } from '../../enums/Roles';
 import { useUserState } from '../../states/UserState';
 import { CompanyTable } from '../../tables/company/CompanyTable';
@@ -19,17 +25,56 @@ import { ManufacturerPartTable } from '../../tables/purchasing/ManufacturerPartT
 import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable';
 import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable';
 
+function PurchaseOrderOverview({
+  view
+}: {
+  view: string;
+}) {
+  switch (view) {
+    case 'calendar':
+      return (
+        <OrderCalendar
+          model={ModelType.purchaseorder}
+          role={UserRoles.purchase_order}
+          params={{ outstanding: true }}
+        />
+      );
+    case 'table':
+    default:
+      return <PurchaseOrderTable />;
+  }
+}
+
 export default function PurchasingIndex() {
   const user = useUserState();
 
+  const [purchaseOrderView, setpurchaseOrderView] = useLocalStorage<string>({
+    key: 'purchaseOrderView',
+    defaultValue: 'table'
+  });
+
   const panels = useMemo(() => {
     return [
       {
         name: 'purchaseorders',
         label: t`Purchase Orders`,
         icon: <IconShoppingCart />,
-        content: <PurchaseOrderTable />,
-        hidden: !user.hasViewRole(UserRoles.purchase_order)
+        hidden: !user.hasViewRole(UserRoles.purchase_order),
+        content: <PurchaseOrderOverview view={purchaseOrderView} />,
+        controls: (
+          <SegmentedIconControl
+            value={purchaseOrderView}
+            onChange={setpurchaseOrderView}
+            data={[
+              { value: 'table', label: t`Table View`, icon: <IconTable /> },
+              {
+                value: 'calendar',
+                label: t`Calendar View`,
+                icon: <IconCalendar />
+              }
+            ]}
+          />
+        )
       },
       {
         name: 'suppliers',
@@ -66,7 +111,7 @@ export default function PurchasingIndex() {
         content: <ManufacturerPartTable params={{}} />
       }
     ];
-  }, [user]);
+  }, [user, purchaseOrderView]);
 
   if (!user.isLoggedIn() || !user.hasViewRole(UserRoles.purchase_order)) {
     return <PermissionDenied />;
diff --git a/src/frontend/src/pages/sales/SalesIndex.tsx b/src/frontend/src/pages/sales/SalesIndex.tsx
index 3b0f26815b..4fab306a59 100644
--- a/src/frontend/src/pages/sales/SalesIndex.tsx
+++ b/src/frontend/src/pages/sales/SalesIndex.tsx
@@ -2,37 +2,121 @@ import { t } from '@lingui/macro';
 import { Stack } from '@mantine/core';
 import {
   IconBuildingStore,
+  IconCalendar,
+  IconTable,
   IconTruckDelivery,
   IconTruckReturn
 } from '@tabler/icons-react';
 import { useMemo } from 'react';
 
+import { useLocalStorage } from '@mantine/hooks';
+import SegmentedIconControl from '../../components/buttons/SegmentedIconControl';
+import OrderCalendar from '../../components/calendar/OrderCalendar';
 import PermissionDenied from '../../components/errors/PermissionDenied';
 import { PageDetail } from '../../components/nav/PageDetail';
 import { PanelGroup } from '../../components/panels/PanelGroup';
+import { ModelType } from '../../enums/ModelType';
 import { UserRoles } from '../../enums/Roles';
 import { useUserState } from '../../states/UserState';
 import { CompanyTable } from '../../tables/company/CompanyTable';
 import { ReturnOrderTable } from '../../tables/sales/ReturnOrderTable';
 import { SalesOrderTable } from '../../tables/sales/SalesOrderTable';
 
-export default function PurchasingIndex() {
+function SalesOrderOverview({
+  view
+}: {
+  view: string;
+}) {
+  switch (view) {
+    case 'calendar':
+      return (
+        <OrderCalendar
+          model={ModelType.salesorder}
+          role={UserRoles.sales_order}
+          params={{ outstanding: true }}
+        />
+      );
+    case 'table':
+    default:
+      return <SalesOrderTable />;
+  }
+}
+
+function ReturnOrderOverview({
+  view
+}: {
+  view: string;
+}) {
+  switch (view) {
+    case 'calendar':
+      return (
+        <OrderCalendar
+          model={ModelType.returnorder}
+          role={UserRoles.return_order}
+          params={{ outstanding: true }}
+        />
+      );
+    case 'table':
+    default:
+      return <ReturnOrderTable />;
+  }
+}
+
+export default function SalesIndex() {
   const user = useUserState();
 
+  const [salesOrderView, setSalesOrderView] = useLocalStorage<string>({
+    key: 'salesOrderView',
+    defaultValue: 'table'
+  });
+
+  const [returnOrderView, setReturnOrderView] = useLocalStorage<string>({
+    key: 'returnOrderView',
+    defaultValue: 'table'
+  });
+
   const panels = useMemo(() => {
     return [
       {
         name: 'salesorders',
         label: t`Sales Orders`,
         icon: <IconTruckDelivery />,
-        content: <SalesOrderTable />,
+        content: <SalesOrderOverview view={salesOrderView} />,
+        controls: (
+          <SegmentedIconControl
+            value={salesOrderView}
+            onChange={setSalesOrderView}
+            data={[
+              { value: 'table', label: t`Table View`, icon: <IconTable /> },
+              {
+                value: 'calendar',
+                label: t`Calendar View`,
+                icon: <IconCalendar />
+              }
+            ]}
+          />
+        ),
         hidden: !user.hasViewRole(UserRoles.sales_order)
       },
       {
         name: 'returnorders',
         label: t`Return Orders`,
         icon: <IconTruckReturn />,
-        content: <ReturnOrderTable />,
+        content: <ReturnOrderOverview view={returnOrderView} />,
+        controls: (
+          <SegmentedIconControl
+            value={returnOrderView}
+            onChange={setReturnOrderView}
+            data={[
+              { value: 'table', label: t`Table View`, icon: <IconTable /> },
+              {
+                value: 'calendar',
+                label: t`Calendar View`,
+                icon: <IconCalendar />
+              }
+            ]}
+          />
+        ),
         hidden: !user.hasViewRole(UserRoles.return_order)
       },
       {
@@ -44,7 +128,7 @@ export default function PurchasingIndex() {
         )
       }
     ];
-  }, [user]);
+  }, [user, salesOrderView, returnOrderView]);
 
   if (!user.isLoggedIn() || !user.hasViewRole(UserRoles.sales_order)) {
     return <PermissionDenied />;
diff --git a/src/frontend/src/pages/sales/SalesOrderShipmentDetail.tsx b/src/frontend/src/pages/sales/SalesOrderShipmentDetail.tsx
index af149a82aa..bcf40c747f 100644
--- a/src/frontend/src/pages/sales/SalesOrderShipmentDetail.tsx
+++ b/src/frontend/src/pages/sales/SalesOrderShipmentDetail.tsx
@@ -4,6 +4,7 @@ import { IconBookmark, IconInfoCircle } from '@tabler/icons-react';
 import { useMemo } from 'react';
 import { useNavigate, useParams } from 'react-router-dom';
 
+import dayjs from 'dayjs';
 import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
 import { PrintingActions } from '../../components/buttons/PrintingActions';
 import {
@@ -263,7 +264,7 @@ export default function SalesOrderShipmentDetail() {
     focus: 'tracking_number',
     initialData: {
       ...shipment,
-      shipment_date: new Date().toISOString().split('T')[0]
+      shipment_date: dayjs().format('YYYY-MM-DD')
     },
     onFormSuccess: refreshShipment
   });
diff --git a/src/frontend/src/tables/DownloadAction.tsx b/src/frontend/src/tables/DownloadAction.tsx
index c7ec67fe7d..239aa1c219 100644
--- a/src/frontend/src/tables/DownloadAction.tsx
+++ b/src/frontend/src/tables/DownloadAction.tsx
@@ -15,7 +15,7 @@ import {
 export function DownloadAction({
   downloadCallback
 }: Readonly<{
-  downloadCallback: (fileFormat: string) => void;
+  downloadCallback?: (fileFormat: string) => void;
 }>) {
   const formatOptions = [
     { value: 'csv', label: t`CSV`, icon: <IconFileTypeCsv /> },
@@ -27,13 +27,14 @@ export function DownloadAction({
     return formatOptions.map((format) => ({
       name: format.label,
       icon: format.icon,
-      onClick: () => downloadCallback(format.value)
+      onClick: () => downloadCallback?.(format.value)
     }));
   }, [formatOptions, downloadCallback]);
 
   return (
     <ActionDropdown
       tooltip={t`Download Data`}
+      tooltipPosition='top-end'
       icon={<IconDownload />}
       actions={actions}
     />
diff --git a/src/frontend/src/tables/Filter.tsx b/src/frontend/src/tables/Filter.tsx
index 3437ec473d..6a3ca357f7 100644
--- a/src/frontend/src/tables/Filter.tsx
+++ b/src/frontend/src/tables/Filter.tsx
@@ -335,3 +335,14 @@ export function IssuedByFilter(): TableFilter {
     description: t`Filter by user who issued the order`
   });
 }
+
+export function PartCategoryFilter(): TableFilter {
+  return {
+    name: 'category',
+    label: t`Category`,
+    description: t`Filter by part category`,
+    apiUrl: apiUrl(ApiEndpoints.category_list),
+    model: ModelType.partcategory,
+    modelRenderer: (instance: any) => instance.name
+  };
+}
diff --git a/src/frontend/src/tables/FilterSelectDrawer.tsx b/src/frontend/src/tables/FilterSelectDrawer.tsx
index 8db116da91..00ab03fae5 100644
--- a/src/frontend/src/tables/FilterSelectDrawer.tsx
+++ b/src/frontend/src/tables/FilterSelectDrawer.tsx
@@ -21,7 +21,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
 import { IconCheck } from '@tabler/icons-react';
 import { StandaloneField } from '../components/forms/StandaloneField';
 import { StylishText } from '../components/items/StylishText';
-import type { TableState } from '../hooks/UseTable';
+import type { FilterSetState } from '../hooks/UseFilterSet';
 import {
   type TableFilter,
   type TableFilterChoice,
@@ -34,16 +34,16 @@ import {
  */
 function FilterItem({
   flt,
-  tableState
+  filterSet
 }: Readonly<{
   flt: TableFilter;
-  tableState: TableState;
+  filterSet: FilterSetState;
 }>) {
   const removeFilter = useCallback(() => {
-    const newFilters = tableState.activeFilters.filter(
+    const newFilters = filterSet.activeFilters.filter(
       (f) => f.name !== flt.name
     );
-    tableState.setActiveFilters(newFilters);
+    filterSet.setActiveFilters(newFilters);
   }, [flt]);
 
   return (
@@ -93,7 +93,7 @@ function FilterElement({
     case 'api':
       return (
         <StandaloneField
-          fieldName={`filter_value_${filterName}`}
+          fieldName={`filter-${filterName}`}
           fieldDefinition={{
             field_type: 'related field',
             api_url: filterProps.apiUrl,
@@ -154,19 +154,18 @@ function FilterElement({
 }
 
 function FilterAddGroup({
-  tableState,
+  filterSet,
   availableFilters
 }: Readonly<{
-  tableState: TableState;
+  filterSet: FilterSetState;
   availableFilters: TableFilter[];
 }>) {
   const filterOptions: TableFilterChoice[] = useMemo(() => {
     // List of filter names which are already active on this table
     let activeFilterNames: string[] = [];
 
-    if (tableState.activeFilters && tableState.activeFilters.length > 0) {
-      activeFilterNames =
-        tableState.activeFilters?.map((flt) => flt.name) ?? [];
+    if (filterSet.activeFilters && filterSet.activeFilters.length > 0) {
+      activeFilterNames = filterSet.activeFilters?.map((flt) => flt.name) ?? [];
     }
 
     return (
@@ -179,7 +178,7 @@ function FilterAddGroup({
           description: flt.description
         })) ?? []
     );
-  }, [tableState.activeFilters, availableFilters]);
+  }, [filterSet.activeFilters, availableFilters]);
 
   const [selectedFilter, setSelectedFilter] = useState<string | null>(null);
 
@@ -232,9 +231,8 @@ function FilterAddGroup({
       }
 
       const filters =
-        tableState.activeFilters?.filter(
-          (flt) => flt.name !== selectedFilter
-        ) ?? [];
+        filterSet.activeFilters?.filter((flt) => flt.name !== selectedFilter) ??
+        [];
 
       const newFilter: TableFilter = {
         ...filter,
@@ -243,7 +241,7 @@ function FilterAddGroup({
           displayValue ?? valueOptions.find((v) => v.value === value)?.label
       };
 
-      tableState.setActiveFilters([...filters, newFilter]);
+      filterSet.setActiveFilters([...filters, newFilter]);
 
       // Clear selected filter
       setSelectedFilter(null);
@@ -275,13 +273,15 @@ function FilterAddGroup({
 }
 
 export function FilterSelectDrawer({
+  title,
   availableFilters,
-  tableState,
+  filterSet,
   opened,
   onClose
 }: Readonly<{
+  title?: string;
   availableFilters: TableFilter[];
-  tableState: TableState;
+  filterSet: FilterSetState;
   opened: boolean;
   onClose: () => void;
 }>) {
@@ -290,13 +290,13 @@ export function FilterSelectDrawer({
   // Hide the "add filter" selection whenever the selected filters change
   useEffect(() => {
     setAddFilter(false);
-  }, [tableState.activeFilters]);
+  }, [filterSet.activeFilters]);
 
   const hasFilters: boolean = useMemo(() => {
-    const filters = tableState?.activeFilters ?? [];
+    const filters = filterSet?.activeFilters ?? [];
 
     return filters.length > 0;
-  }, [tableState.activeFilters]);
+  }, [filterSet.activeFilters]);
 
   return (
     <Drawer
@@ -308,18 +308,18 @@ export function FilterSelectDrawer({
       closeButtonProps={{
         'aria-label': 'filter-drawer-close'
       }}
-      title={<StylishText size='lg'>{t`Table Filters`}</StylishText>}
+      title={<StylishText size='lg'>{title ?? t`Table Filters`}</StylishText>}
     >
       <Stack gap='xs'>
         {hasFilters &&
-          tableState.activeFilters?.map((f) => (
-            <FilterItem key={f.name} flt={f} tableState={tableState} />
+          filterSet.activeFilters?.map((f) => (
+            <FilterItem key={f.name} flt={f} filterSet={filterSet} />
           ))}
         {hasFilters && <Divider />}
         {addFilter && (
           <Stack gap='xs'>
             <FilterAddGroup
-              tableState={tableState}
+              filterSet={filterSet}
               availableFilters={availableFilters}
             />
           </Stack>
@@ -334,7 +334,7 @@ export function FilterSelectDrawer({
           </Button>
         )}
         {!addFilter &&
-          tableState.activeFilters.length < availableFilters.length && (
+          filterSet.activeFilters.length < availableFilters.length && (
             <Button
               onClick={() => setAddFilter(true)}
               color='green'
@@ -343,9 +343,9 @@ export function FilterSelectDrawer({
               <Text>{t`Add Filter`}</Text>
             </Button>
           )}
-        {!addFilter && tableState.activeFilters.length > 0 && (
+        {!addFilter && filterSet.activeFilters.length > 0 && (
           <Button
-            onClick={tableState.clearActiveFilters}
+            onClick={filterSet.clearActiveFilters}
             color='red'
             variant='subtle'
           >
diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx
index 79b9c44b17..637da628c8 100644
--- a/src/frontend/src/tables/InvenTreeTable.tsx
+++ b/src/frontend/src/tables/InvenTreeTable.tsx
@@ -359,8 +359,8 @@ export function InvenTreeTable<T extends Record<string, any>>({
       };
 
       // Add custom filters
-      if (tableState.activeFilters) {
-        tableState.activeFilters.forEach((flt) => {
+      if (tableState.filterSet.activeFilters) {
+        tableState.filterSet.activeFilters.forEach((flt) => {
           queryParams[flt.name] = flt.value;
         });
       }
@@ -401,7 +401,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
     [
       tableProps.params,
       tableProps.enablePagination,
-      tableState.activeFilters,
+      tableState.filterSet.activeFilters,
       tableState.queryFilters,
       tableState.searchTerm,
       tableState.pageSize,
@@ -524,7 +524,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
       sortStatus.columnAccessor,
       sortStatus.direction,
       tableState.tableKey,
-      tableState.activeFilters,
+      tableState.filterSet.activeFilters,
       tableState.searchTerm
     ],
     enabled: !!url && !tableData,
@@ -629,7 +629,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
     }
   };
 
-  // pagination refresth table if pageSize changes
+  // pagination refresh table if pageSize changes
   function updatePageSize(newData: number) {
     tableState.setPageSize(newData);
     tableState.setPage(1);
diff --git a/src/frontend/src/tables/InvenTreeTableHeader.tsx b/src/frontend/src/tables/InvenTreeTableHeader.tsx
index bfc1d15894..19daef8a32 100644
--- a/src/frontend/src/tables/InvenTreeTableHeader.tsx
+++ b/src/frontend/src/tables/InvenTreeTableHeader.tsx
@@ -63,8 +63,8 @@ export default function InvenTreeTableHeader({
     };
 
     // Add in active filters
-    if (tableState.activeFilters) {
-      tableState.activeFilters.forEach((filter) => {
+    if (tableState.filterSet.activeFilters) {
+      tableState.filterSet.activeFilters.forEach((filter) => {
         queryParams[filter.name] = filter.value;
       });
     }
@@ -141,7 +141,7 @@ export default function InvenTreeTableHeader({
         <Boundary label={`InvenTreeTableFilterDrawer-${tableState.tableKey}`}>
           <FilterSelectDrawer
             availableFilters={filters}
-            tableState={tableState}
+            filterSet={tableState.filterSet}
             opened={filtersVisible}
             onClose={() => setFiltersVisible(false)}
           />
@@ -216,8 +216,8 @@ export default function InvenTreeTableHeader({
           {tableProps.enableFilters && filters.length > 0 && (
             <Indicator
               size='xs'
-              label={tableState.activeFilters?.length ?? 0}
-              disabled={tableState.activeFilters?.length == 0}
+              label={tableState.filterSet.activeFilters?.length ?? 0}
+              disabled={tableState.filterSet.activeFilters?.length == 0}
             >
               <ActionIcon
                 disabled={hasCustomFilters}
diff --git a/src/frontend/src/tables/Search.tsx b/src/frontend/src/tables/Search.tsx
index 3f6acc7dd3..9051fc687d 100644
--- a/src/frontend/src/tables/Search.tsx
+++ b/src/frontend/src/tables/Search.tsx
@@ -28,7 +28,13 @@ export function TableSearchInput({
       onChange={(event) => setValue(event.target.value)}
       rightSection={
         value.length > 0 ? (
-          <CloseButton size='xs' onClick={() => setValue('')} />
+          <CloseButton
+            size='xs'
+            onClick={() => {
+              setValue('');
+              searchCallback('');
+            }}
+          />
         ) : null
       }
     />
diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx
index 8ebe095753..c4e5d11339 100644
--- a/src/frontend/src/tables/build/BuildOrderTable.tsx
+++ b/src/frontend/src/tables/build/BuildOrderTable.tsx
@@ -36,6 +36,7 @@ import {
   OrderStatusFilter,
   OutstandingFilter,
   OverdueFilter,
+  PartCategoryFilter,
   ProjectCodeFilter,
   ResponsibleFilter,
   StartDateAfterFilter,
@@ -154,14 +155,7 @@ export function BuildOrderTable({
       HasProjectCodeFilter(),
       IssuedByFilter(),
       ResponsibleFilter(),
-      {
-        name: 'category',
-        label: t`Category`,
-        description: t`Filter by part category`,
-        apiUrl: apiUrl(ApiEndpoints.category_list),
-        model: ModelType.partcategory,
-        modelRenderer: (instance: any) => instance.name
-      }
+      PartCategoryFilter()
     ];
 
     // If we are filtering on a specific part, we can include the "include variants" filter
diff --git a/src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx b/src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx
index d9e3de429a..244282b53b 100644
--- a/src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx
+++ b/src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx
@@ -3,6 +3,7 @@ import { IconTruckDelivery } from '@tabler/icons-react';
 import { useCallback, useMemo, useState } from 'react';
 import { useNavigate } from 'react-router-dom';
 
+import dayjs from 'dayjs';
 import { AddItemButton } from '../../components/buttons/AddItemButton';
 import { YesNoButton } from '../../components/buttons/YesNoButton';
 import { ApiEndpoints } from '../../enums/ApiEndpoints';
@@ -82,7 +83,7 @@ export default function SalesOrderShipmentTable({
     focus: 'tracking_number',
     initialData: {
       ...selectedShipment,
-      shipment_date: new Date().toISOString().split('T')[0]
+      shipment_date: dayjs().format('YYYY-MM-DD')
     }
   });
 
diff --git a/src/frontend/tests/helpers.ts b/src/frontend/tests/helpers.ts
index 46aba50ef9..68b55f4afd 100644
--- a/src/frontend/tests/helpers.ts
+++ b/src/frontend/tests/helpers.ts
@@ -101,6 +101,18 @@ export const loadTab = async (page, tabName) => {
   await page.waitForLoadState('networkidle');
 };
 
+// Activate "table" view in certain contexts
+export const activateTableView = async (page) => {
+  await page.getByLabel('segmented-icon-control-table').click();
+  await page.waitForLoadState('networkidle');
+};
+
+// Activate "calendar" view in certain contexts
+export const activateCalendarView = async (page) => {
+  await page.getByLabel('segmented-icon-control-calendar').click();
+  await page.waitForLoadState('networkidle');
+};
+
 /**
  * Perform a 'global search' on the provided page, for the provided query text
  */
diff --git a/src/frontend/tests/pages/pui_build.spec.ts b/src/frontend/tests/pages/pui_build.spec.ts
index 244b57c833..0925f9e69a 100644
--- a/src/frontend/tests/pages/pui_build.spec.ts
+++ b/src/frontend/tests/pages/pui_build.spec.ts
@@ -1,6 +1,7 @@
 import { expect } from '@playwright/test';
 import { test } from '../baseFixtures.ts';
 import {
+  activateCalendarView,
   clearTableFilters,
   getRowFromCell,
   loadTab,
@@ -90,6 +91,21 @@ test('Build Order - Basic Tests', async ({ page }) => {
     .waitFor();
 });
 
+test('Build Order - Calendar', async ({ page }) => {
+  await doQuickLogin(page);
+
+  await navigate(page, 'manufacturing/index/buildorders');
+  await activateCalendarView(page);
+
+  // Check "part category" filter
+  await page.getByLabel('calendar-select-filters').click();
+  await page.getByRole('button', { name: 'Add Filter' }).click();
+  await page.getByPlaceholder('Select filter').fill('category');
+  await page.getByRole('option', { name: 'Category', exact: true }).click();
+  await page.getByLabel('related-field-filter-category').click();
+  await page.getByText('Part category, level 1').waitFor();
+});
+
 test('Build Order - Edit', async ({ page }) => {
   await doQuickLogin(page);
 
diff --git a/src/frontend/tests/pages/pui_purchase_order.spec.ts b/src/frontend/tests/pages/pui_purchase_order.spec.ts
index 47e3a73769..6ed9189dc4 100644
--- a/src/frontend/tests/pages/pui_purchase_order.spec.ts
+++ b/src/frontend/tests/pages/pui_purchase_order.spec.ts
@@ -1,6 +1,8 @@
 import { expect } from '@playwright/test';
 import { test } from '../baseFixtures.ts';
 import {
+  activateCalendarView,
+  activateTableView,
   clearTableFilters,
   clickButtonIfVisible,
   clickOnRowMenu,
@@ -11,11 +13,12 @@ import {
 } from '../helpers.ts';
 import { doQuickLogin } from '../login.ts';
 
-test('Purchase Orders - List', async ({ page }) => {
+test('Purchase Orders - Table', async ({ page }) => {
   await doQuickLogin(page);
 
   await page.getByRole('tab', { name: 'Purchasing' }).click();
   await loadTab(page, 'Purchase Orders');
+  await activateTableView(page);
 
   await clearTableFilters(page);
 
@@ -39,6 +42,30 @@ test('Purchase Orders - List', async ({ page }) => {
   await page.getByText('2025-07-17').waitFor(); // Target Date
 });
 
+test('Purchase Orders - Calendar', async ({ page }) => {
+  await doQuickLogin(page);
+
+  await page.getByRole('tab', { name: 'Purchasing' }).click();
+  await loadTab(page, 'Purchase Orders');
+
+  // Ensure view is in "calendar" mode
+  await activateCalendarView(page);
+
+  // Check for expected components
+  await page.getByLabel('action-button-previous-month').waitFor();
+  await page.getByLabel('action-button-next-month').waitFor();
+
+  await page.getByLabel('calendar-select-month').click();
+  await page.getByRole('button', { name: 'Jan' }).waitFor();
+  await page.getByRole('button', { name: 'Feb' }).waitFor();
+  await page.getByRole('button', { name: 'Dec' }).click();
+
+  await page.getByText('December').waitFor();
+
+  // Put back into table view
+  await activateTableView(page);
+});
+
 test('Purchase Orders - Barcodes', async ({ page }) => {
   await doQuickLogin(page);
 
@@ -157,6 +184,7 @@ test('Purchase Orders - Filters', async ({ page }) => {
 
   await page.getByRole('tab', { name: 'Purchasing' }).click();
   await loadTab(page, 'Purchase Orders');
+  await activateTableView(page);
 
   // Open filters drawer
   await openFilterDrawer(page);
diff --git a/src/frontend/tests/pui_printing.spec.ts b/src/frontend/tests/pui_printing.spec.ts
index 917ee149b3..ae50369b9a 100644
--- a/src/frontend/tests/pui_printing.spec.ts
+++ b/src/frontend/tests/pui_printing.spec.ts
@@ -1,5 +1,5 @@
 import { expect, test } from './baseFixtures.js';
-import { loadTab, navigate } from './helpers.js';
+import { activateTableView, loadTab, navigate } from './helpers.js';
 import { doQuickLogin } from './login.js';
 import { setPluginState } from './settings.js';
 
@@ -59,6 +59,7 @@ test('Report Printing', async ({ page }) => {
   // Navigate to a specific PurchaseOrder
   await page.getByRole('tab', { name: 'Purchasing' }).click();
   await loadTab(page, 'Purchase Orders');
+  await activateTableView(page);
 
   await page.getByRole('cell', { name: 'PO0009' }).click();
 
diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock
index 44e87a6c50..684b678365 100644
--- a/src/frontend/yarn.lock
+++ b/src/frontend/yarn.lock
@@ -1311,6 +1311,28 @@
   dependencies:
     prop-types "^15.8.1"
 
+"@fullcalendar/core@^6.1.15":
+  version "6.1.15"
+  resolved "https://registry.yarnpkg.com/@fullcalendar/core/-/core-6.1.15.tgz#6c3f5259fc4589870228853072131219bb533f6e"
+  integrity sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q==
+  dependencies:
+    preact "~10.12.1"
+
+"@fullcalendar/daygrid@^6.1.15":
+  version "6.1.15"
+  resolved "https://registry.yarnpkg.com/@fullcalendar/daygrid/-/daygrid-6.1.15.tgz#91208b0955ba805ddad285a53ee6f53855146963"
+  integrity sha512-j8tL0HhfiVsdtOCLfzK2J0RtSkiad3BYYemwQKq512cx6btz6ZZ2RNc/hVnIxluuWFyvx5sXZwoeTJsFSFTEFA==
+
+"@fullcalendar/interaction@^6.1.15":
+  version "6.1.15"
+  resolved "https://registry.yarnpkg.com/@fullcalendar/interaction/-/interaction-6.1.15.tgz#1c685d5c269388d4877b75ab2185e97d7c386cc7"
+  integrity sha512-DOTSkofizM7QItjgu7W68TvKKvN9PSEEvDJceyMbQDvlXHa7pm/WAVtAc6xSDZ9xmB1QramYoWGLHkCYbTW1rQ==
+
+"@fullcalendar/react@^6.1.15":
+  version "6.1.15"
+  resolved "https://registry.yarnpkg.com/@fullcalendar/react/-/react-6.1.15.tgz#3198b4a64e256afd37c9760c8741a9af89ade894"
+  integrity sha512-L0b9hybS2J4e7lq6G2CD4nqriyLEqOH1tE8iI6JQjAMTVh5JicOo5Mqw+fhU5bJ7hLfMw2K3fksxX3Ul1ssw5w==
+
 "@istanbuljs/load-nyc-config@^1.0.0", "@istanbuljs/load-nyc-config@^1.1.0":
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@@ -4128,6 +4150,11 @@ postcss@^8.4.43, postcss@^8.4.49:
     picocolors "^1.1.1"
     source-map-js "^1.2.1"
 
+preact@~10.12.1:
+  version "10.12.1"
+  resolved "https://registry.yarnpkg.com/preact/-/preact-10.12.1.tgz#8f9cb5442f560e532729b7d23d42fd1161354a21"
+  integrity sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==
+
 pretty-format@^29.7.0:
   version "29.7.0"
   resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812"