I want to list data in admin panel

I’m creating a module in Magento 2 for managing flash sale events.
My current goal is to list all the events in an admin grid
But nothing is displayed on the page.

./etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="MagentoFrameworkViewElementUiComponentDataProviderCollectionFactory">
        <arguments>
            <argument name="collections" xsi:type="array">
                <item name="flash_sale_event_listing_data_source" xsi:type="string">
                    TrainingFlashSaleModelResourceModelFlashSaleEventGridCollection
                </item>
            </argument>
        </arguments>
    </type>

    <virtualType name="TrainingFlashSaleModelResourceModelFlashSaleEventGridCollection"
                 type="MagentoFrameworkViewElementUiComponentDataProviderSearchResult">
        <arguments>
            <argument name="mainTable" xsi:type="string">flash_sale_event</argument>
            <argument name="resourceModel" xsi:type="string">TrainingFlashSaleModelResourceModelFlashSaleEvent
            </argument>
            <argument name="model" xsi:type="string">MagentoFrameworkViewElementUiComponentDataProviderDocument
            </argument>
        </arguments>
    </virtualType>

</config>

./etc/adminhtml/acl.xml

<?xml version="1.0"?>
<acl xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
    <resources>
        <resource id="Magento_Backend::admin">
            <resource id="Training_FlashSale::flashsale" title="Flash Sale" sortOrder="10">
                <resource id="Training_FlashSale::manage_flashsale" title="Manage Flash Sales"/>
            </resource>
        </resource>
    </resources>
</acl>

./Model/FlashSaleEvent.php

<?php

namespace TrainingFlashSaleModel;

use MagentoFrameworkDataCollectionAbstractDb;
use MagentoFrameworkModelAbstractModel;
use MagentoFrameworkModelContext;
use MagentoFrameworkModelResourceModelAbstractResource;
use MagentoFrameworkRegistry;
use PsrLogLoggerInterface;

class FlashSaleEvent extends AbstractModel
{
    /**
     * @var LoggerInterface
     */
    private LoggerInterface $logger;

    /**
     * Constructor.
     *
     * @param Context $context
     * @param Registry $registry
     * @param AbstractResource|null $resource
     * @param AbstractDb|null $resourceCollection
     * @param array $data
     */
    public function __construct(
        Context                                                 $context,
        Registry                                                $registry,
        MagentoFrameworkModelResourceModelAbstractResource $resource = null,
        MagentoFrameworkDataCollectionAbstractDb           $resourceCollection = null,
        array                                                   $data = []
    )
    {
        $this->logger = $context->getLogger();
        parent::__construct($context, $registry, $resource, $resourceCollection, $data);
    }

    /**
     * Initialize resource model.
     */
    protected function _construct(): void
    {
        $this->_init(ResourceModelFlashSaleEvent::class);
    }

    /**
     * Check if the flash sale event is active.
     *
     * This method checks that the event's status is enabled and that the current time
     * falls between the start and end dates.
     *
     * @return bool
     */
    public function isActive(): bool
    {
        $status = $this->getData('status');
        // Use the correct keys: start_time and end_time
        $startTime = $this->getData('start_time');
        $endTime = $this->getData('end_time');

        try {
            $currentTime = new DateTime();
            $start = new DateTime($startTime);
            $end = new DateTime($endTime);
        } catch (Exception $e) {
            // Log the error message instead of the object
            $this->logger->critical('Date conversion failed: ' . $e->getMessage());
            return false;
        }

        return ($status == 1 && $currentTime >= $start && $currentTime <= $end);
    }


    /**
     * Retrieve the discount type.
     *
     * Example values: 'percentage' or 'fixed'
     *
     * @return string
     */
    public function getDiscountType(): string
    {
        return (string)$this->getData('discount_type');
    }

    /**
     * Retrieve the discount value.
     *
     * This value is used in conjunction with the discount type to calculate the sale price.
     *
     * @return float
     */
    public function getDiscountValue(): float
    {
        return (float)$this->getData('discount_value');
    }

    /**
     * Get associated product IDs for this flash sale event.
     *
     * Typically, this data might be loaded from the pivot table, and stored in the model
     * (e.g., as an array or a comma-separated string). Here we assume an array is stored.
     *
     * @return array
     */
    public function getAssociatedProductIds(): array
    {
        $productIds = $this->getData('associated_product_ids');
        return is_array($productIds) ? $productIds : [];
    }

    /**
     * (Optional) Compute the discounted price for a given original price.
     *
     * For a percentage discount, it subtracts the computed discount amount.
     * For a fixed discount, it subtracts the fixed amount (not going below zero).
     *
     * @param float $originalPrice
     * @return float
     */
    public function computeDiscountedPrice(float $originalPrice): float
    {
        $discountValue = $this->getDiscountValue();
        $discountType = $this->getDiscountType();

        if ($discountType === 'percentage') {
            $discountAmount = ($discountValue / 100) * $originalPrice;
            return max(0, $originalPrice - $discountAmount);
        } elseif ($discountType === 'fixed') {
            return max(0, $originalPrice - $discountValue);
        }

        // Return original price if discount type is not recognized
        return $originalPrice;
    }
}

./Model/ResourceModel/FlashSaleEvent.php

<?php

namespace TrainingFlashSaleModelResourceModel;

use MagentoFrameworkModelResourceModelDbAbstractDb;
use MagentoFrameworkModelAbstractModel;

class FlashSaleEvent extends AbstractDb
{
    /**
     * Initialize resource model.
     *
     * This method links the model to the database table `flash_sale_event` with the primary key `event_id`.
     */
    protected function _construct(): void
    {
        $logger = MagentoFrameworkAppObjectManager::getInstance()->get(PsrLogLoggerInterface::class);
        $logger->debug('Constructing flashSaleEvent resource model.');
        $this->_init('flash_sale_event', 'event_id');
    }

    /**
     * After-save hook to update the pivot table with associated products.
     *
     * This method checks if the model contains an array of associated product IDs.
     * It then clears any existing associations in the pivot table and inserts the new ones.
     *
     * @param AbstractModel $object
     * @return $this
     */
    protected function _afterSave(AbstractModel $object): static
    {
        $logger = MagentoFrameworkAppObjectManager::getInstance()->get(PsrLogLoggerInterface::class);

        $logger->debug('entering after save.');

        $productIds = $object->getData('associated_product_ids');
        if (is_array($productIds)) {
            $connection = $this->getConnection();
            $pivotTable = $this->getTable('flash_sale_event_product');

            // Remove existing associations for this event
            $connection->delete($pivotTable, ['event_id = ?' => $object->getId()]);

            // Insert each new association
            foreach ($productIds as $productId) {
                $connection->insert($pivotTable, [
                    'event_id' => $object->getId(),
                    'product_id' => $productId,
                ]);
            }
        }
        $logger->debug('leaving after save.');

        return parent::_afterSave($object);
    }
}

./Model/ResourceModel/FlashSaleEvent/Collection.php

<?php

namespace TrainingFlashSaleModelResourceModelFlashSaleEvent;

use MagentoFrameworkModelResourceModelDbCollectionAbstractCollection;
use TrainingFlashSaleModelFlashSaleEvent as FlashSaleEventModel;
use TrainingFlashSaleModelResourceModelFlashSaleEvent as FlashSaleEventResource;

class Collection extends AbstractCollection
{
    /**
     * Initialize the collection by specifying the model and resource model.
     */
    protected function _construct(): void
    {
        $this->_init(FlashSaleEventModel::class, FlashSaleEventResource::class);
    }


    /**
     * Add filter to only include active flash sale events.
     *
     * Active events are those with:
     * - status = 1,
     * - a start time before or equal to the current timestamp,
     * - an end time after or equal to the current timestamp.
     *This method is useful for cron jobs or frontend blocks that need to display only currently active flash sale events
     *
     * @return $this
     */
    public function addActiveFilter(): self
    {
        $currentTimestamp = date('Y-m-d H:i:s');
        $this->getSelect()
            ->where('status = ?', 1)
            ->where('start_time <= ?', $currentTimestamp)
            ->where('end_time >= ?', $currentTimestamp);
        return $this;
    }
}

./view/adminhtml/layout/flashsale_flashsale_index.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="admin-2columns-left"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <uiComponent name="flash_sale_event_listing"/>
    </body>
</page>

./view/adminhtml/ui_component/flash_sale_event_listing.xml

<?xml version="1.0"?>
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <!-- JS configuration for the grid -->
    <argument name="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="provider" xsi:type="string">flash_sale_event_listing.flash_sale_event_listing_data_source</item>
        </item>
    </argument>
    <settings>
        <spinner>event_columns</spinner>
        <deps>
            <dep>flash_sale_event_listing.flash_sale_event_listing_data_source</dep>
        </deps>
    </settings>
    <!-- Data Source configuration -->
    <dataSource name="flash_sale_event_listing_data_source" component="Magento_Ui/js/grid/provider">
        <settings>
            <updateUrl path="mui/index/render"/>
        </settings>
        <dataProvider class="TrainingFlashSaleUiDataProviderFlashSaleEventDataProvider"
                      name="flash_sale_event_listing_data_source">
            <settings>
                <requestFieldName>event_id</requestFieldName>
                <primaryFieldName>event_id</primaryFieldName>
            </settings>
        </dataProvider>
    </dataSource>
    <!--    Listing toolbar for filters, mass actions and paging-->
    <listingToolbar name="listing_top">
        <bookmark name="bookmarks"/>
        <columnsControls name="columns_controls"/>
        <filters name="listing_filters"/>
        <massaction name="listing_massaction"/>
        <paging name="listing_paging"/>
    </listingToolbar>
    <!-- Columns definition -->
    <columns name="event_columns">
        <!-- Selection Checkbox -->
        <selectionsColumn name="ids" sortOrder="0">
            <settings>
                <indexField>event_id</indexField>
            </settings>
        </selectionsColumn>
        <!-- Event ID Column -->
        <column name="event_id" sortOrder="10">
            <settings>
                <filter>textRange</filter>
                <label translate="true">ID</label>
                <sorting>asc</sorting>
            </settings>
        </column>
        <!-- Event Name Column -->
        <column name="name" sortOrder="20">
            <settings>
                <filter>text</filter>
                <label translate="true">Event Name</label>
            </settings>
        </column>
        <!-- Discount Type Column -->
        <column name="discount_type" sortOrder="30">
            <settings>
                <filter>select</filter>
                <dataType>text</dataType>
                <label translate="true">Discount Type</label>
            </settings>
        </column>
        <!-- Discount Value Column -->
        <column name="discount_value" sortOrder="40">
            <settings>
                <filter>textRange</filter>
                <dataType>number</dataType>
                <label translate="true">Discount Value</label>
            </settings>
        </column>
        <!-- Start Time Column -->
        <column name="start_time" sortOrder="50">
            <settings>
                <filter>dateRange</filter>
                <dataType>date</dataType>
                <label translate="true">Start Time</label>
            </settings>
        </column>
        <!-- End Time Column -->
        <column name="end_time" sortOrder="60">
            <settings>
                <filter>dateRange</filter>
                <dataType>date</dataType>
                <label translate="true">End Time</label>
            </settings>
        </column>
        <!-- Actions Column for Edit -->
        <actionsColumn name="actions" sortOrder="100">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="indexField" xsi:type="string">event_id</item>
                    <item name="actions" xsi:type="array">
                        <item name="edit" xsi:type="array">
                            <item name="type" xsi:type="string">edit</item>
                            <item name="label" xsi:type="string" translate="true">Edit</item>
                            <item name="url" xsi:type="url" path="flashsale/event/edit"/>
                        </item>
                    </item>
                </item>
            </argument>
        </actionsColumn>
    </columns>
</listing>

./Ui/DataProvider/FlashSaleEventDataProvider.php

<?php

namespace TrainingFlashSaleUiDataProvider;

use MagentoUiDataProviderAbstractDataProvider;
use TrainingFlashSaleModelResourceModelFlashSaleEventCollection as FlashSaleEventCollection;
use PsrLogLoggerInterface;

class FlashSaleEventDataProvider extends AbstractDataProvider
{
    /**
     * @var FlashSaleEventCollection
     */
    protected $collection;

    /**
     * @var LoggerInterface
     */
    protected LoggerInterface $logger;

    /**
     * Constructor
     *
     * @param string $name
     * @param string $primaryFieldName
     * @param string $requestFieldName
     * @param FlashSaleEventCollection $collection
     * @param LoggerInterface $logger
     * @param array $meta
     * @param array $data
     */
    public function __construct(
        $name,
        $primaryFieldName,
        $requestFieldName,
        FlashSaleEventCollection $collection,
        LoggerInterface $logger,
        array $meta = [],
        array $data = []
    )
    {
        $this->logger = $logger;
        $this->logger->debug('FlashSaleEventDataProvider constructed.');
        $this->collection = $collection;
        $this->logger->debug('Main table: ' . $this->collection->getMainTable());
        parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data);
    }

    /**
     * Override getData() to log the collection details.
     *
     * @return array
     */
    public function getData(): array
    {
        $this->logger->debug('Main table: ' . $this->collection->getMainTable());

        $this->logger->debug('FlashSaleEventDataProvider: getData() called.');

        // Log collection size before loading (if not loaded yet)
        if (!$this->collection->isLoaded()) {
            $this->logger->debug('Collection not loaded yet. Size before load: ' . $this->collection->getSize());
        }

        // Force loading the collection
        $this->collection->load();
        $itemsCount = $this->collection->getSize();
        $this->logger->debug('After load, collection size: ' . $itemsCount);

        $sql = $this->collection->getSelect()->__toString();
        $this->logger->debug('SQL Query: ' . $sql);

        $data = [
            'totalRecords' => $itemsCount,
            'items' => array_values($this->collection->toArray()['items'])
        ];
        $this->logger->debug('Data fetched: ' . json_encode($data));

        return $data;
    }

}

./Controller/Adminhtml/FlashSale/Index.php

<?php

namespace TrainingFlashSaleControllerAdminhtmlFlashSale;

use MagentoBackendAppAction;
use MagentoFrameworkAppResponseInterface;
use MagentoFrameworkControllerResultInterface;
use MagentoFrameworkViewResultPage;
use MagentoFrameworkViewResultPageFactory;
use PsrLogLoggerInterface;

class Index extends Action
{
    const ADMIN_RESOURCE = 'Training_FlashSale::flashsale';

    /**
     * @var PageFactory
     */
    protected PageFactory $resultPageFactory;

    private LoggerInterface $logger;

    public function __construct(
        ActionContext  $context,
        PageFactory     $resultPageFactory,
        LoggerInterface $logger
    )
    {
        parent::__construct($context);
        $this->resultPageFactory = $resultPageFactory;
        $this->logger = $logger;
    }

    public function execute(): Page|ResultInterface|ResponseInterface
    {
        $this->logger->debug("Starting FlashSale Event");
        $resultPage = $this->resultPageFactory->create();
        $resultPage->setActiveMenu('Training_FlashSale::flashsale');
        $resultPage->getConfig()->getTitle()->prepend(__('Manage Flash Sales'));
        return $resultPage;
    }
}