Skip to content

[Products]: Allow the user to upload/insert a product image that will be shown in quotes and invoices. #63

Open

Description

@nielsdrost7

Context


Allow the user to upload/insert a product image that will be shown in quotes and invoices.


Specific

As an administrator or sales representative, I want to upload and associate a unique product image to each product record within the product management module.

Measurable

The success of this feature will be measured by the automatic display of the uploaded image on 100% of generated quotes and invoices, eliminating manual image insertion.

Achievable

Given existing file upload functionalities and the defined scope, implementing this image association and display within the Laravel framework is a realistic goal.

Relevant

This feature is highly relevant as it directly enhances the professionalism and visual clarity of customer-facing documents, supporting sales efforts and reducing operational overhead.

Time-bound

This functionality should be fully implemented, tested, and deployed within the next sprint to immediately impact document generation efficiency.


Arrange

  • The user is logged in as an administrator.
  • A product record, e.g., "Product X," exists in the system without an associated image.
  • A valid product image file (e.g., product_x.png) is available on the user's local machine.
  • The system is configured to display product images on quotes and invoices.

Act

  • The user navigates to the product management module.
  • The user selects "Product X" for editing.
  • The user clicks on the "Upload Image" button/field for "Product X".
  • The user selects product_x.png from their local machine and confirms the upload.
  • The user saves the changes to "Product X".
  • The user generates a new quote or invoice that includes "Product X".

Assert

  • The uploaded image product_x.png is successfully associated with "Product X" in the system.
  • The uploaded image is visible within the product's details view in the product management module.
  • The generated quote clearly displays product_x.png next to "Product X".
  • The generated invoice clearly displays product_x.png next to "Product X".
  • No error messages are displayed during the upload or generation process.

Activity

  1. added this to the v2.0.0-alpha milestone on Dec 28, 2025
  2. nielsdrost7 commented on Jan 2, 2026

    @nielsdrost7
    CollaboratorAuthor

    @coderabbitai make pull-request with AAA test Scenario's: Arrange, Act, Assert. And also make sure you test edge cases

  3. coderabbitai commented on Jan 2, 2026

    @coderabbitai
    Contributor

    Rate Limit Exceeded

    @nielsdrost7 have exceeded the limit for the number of chat messages per hour. Please wait 0 minutes and 32 seconds before sending another message.

  4. nielsdrost7 commented on Jul 16, 2026

    @nielsdrost7
    CollaboratorAuthor

    SMART Plan — Product images on quotes and invoices

    Context/Epic: Products

    Specific

    Let a company upload an image per product and have it appear automatically on generated quotes and invoices. Current state: Modules\Products\Models\Product has no image field or upload relationship at all — a repo-wide search for image/photo inside Modules/Products returns nothing. Two existing building blocks could support this: (1) Modules\Core\Models\Upload — a generic polymorphic model (uploadable_type/uploadable_id via morphTo(), company-scoped) that is fully defined but currently used by zero models anywhere in the codebase (no morphMany(Upload::class, 'uploadable') exists on any model today), or (2) the simpler precedent already used for company branding — Modules/Core/Filament/Admin/Pages/Settings.php uses plain FileUpload::make('settings.invoice_logo') storing a path directly in a settings JSON blob. Given a product needs exactly one primary image (per the issue's Arrange/Act/Assert), the simpler FileUpload-on-the-model pattern is the better fit than standing up the unused generic Upload polymorphic system for a single-image use case. The invoice/quote PDF templates (Modules/Core/resources/Templates/Views/templates/{invoices,quotes}/default.blade.php) currently render a plain <table> of item rows with no image column — the templates themselves need a new column, not just backend data.

    Measurable

    • Products gain a nullable image (storage path) column, editable via a FileUpload field on the product form.
      • PHPUnit: it_stores_an_uploaded_image_path_on_a_product()
        #[Test]
        public function it_stores_an_uploaded_image_path_on_a_product(): void
        {
            /* Arrange */
            Storage::fake('public');
            $file = UploadedFile::fake()->image('product_x.png');
    
            /* Act */
            $product = app(ProductService::class)->createProduct([
                'category_id'  => ProductCategory::factory()->for($this->company)->create()->id,
                'type'         => ProductType::PRODUCT->value,
                'code'         => 'SKU-X',
                'product_name' => 'Product X',
                'price'        => 10,
                'image'        => $file->store('products', 'public'),
            ]);
    
            /* Assert */
            $this->assertNotNull($product->image);
            Storage::disk('public')->assertExists($product->image);
        }
    • Frontend/Livewire: As an administrator I want to upload a product image through the product modal so that it's saved without leaving the product list.
        #[Test]
        public function admin_can_upload_a_product_image_through_the_edit_modal(): void
        {
            /* Arrange */
            Storage::fake('public');
            $product = Product::factory()->for($this->company)->create(['image' => null]);
            $file    = UploadedFile::fake()->image('product_x.png');
    
            /* Act */
            $component = Livewire::actingAs($this->user)
                ->test(ListProducts::class)
                ->mountTableAction('edit', $product)
                ->fillForm(['image' => $file])
                ->callMountedTableAction();
    
            /* Assert */
            $component->assertHasNoFormErrors();
            $this->assertNotNull($product->fresh()->image);
        }
    • The product's image renders on its own detail view (product list row / view modal).
      • PHPUnit: it_exposes_a_public_url_for_the_products_image()
        #[Test]
        public function it_exposes_a_public_url_for_the_products_image(): void
        {
            /* Arrange */
            Storage::fake('public');
            $product = Product::factory()->for($this->company)->create(['image' => 'products/widget.png']);
    
            /* Act */
            $url = $product->image_url;
    
            /* Assert */
            $this->assertStringContainsString('products/widget.png', $url);
        }
    • The image appears on the invoice PDF next to its line item.
      • PHPUnit: it_renders_the_product_image_on_the_invoice_pdf()
        #[Test]
        public function it_renders_the_product_image_on_the_invoice_pdf(): void
        {
            /* Arrange */
            Storage::fake('public');
            $product = Product::factory()->for($this->company)->create(['image' => 'products/widget.png']);
            $invoice = Invoice::factory()->for($this->company)->create();
            InvoiceItem::factory()->for($invoice)->create(['product_id' => $product->id]);
    
            /* Act */
            $response = $this->get(route('filament.company.resources.invoices.view', ['tenant' => Str::lower($this->company->search_code), 'record' => $invoice->id]));
    
            /* Assert */
            $response->assertSee('products/widget.png', false);
        }
    • A product with no image renders quotes/invoices without a broken image tag or error.
      • PHPUnit: it_renders_the_invoice_pdf_without_error_when_the_product_has_no_image()
        #[Test]
        public function it_renders_the_invoice_pdf_without_error_when_the_product_has_no_image(): void
        {
            /* Arrange */
            $product = Product::factory()->for($this->company)->create(['image' => null]);
            $invoice = Invoice::factory()->for($this->company)->create();
            InvoiceItem::factory()->for($invoice)->create(['product_id' => $product->id]);
    
            /* Act */
            $response = $this->get(route('filament.company.resources.invoices.view', ['tenant' => Str::lower($this->company->search_code), 'record' => $invoice->id]));
    
            /* Assert */
            $response->assertSuccessful();
        }

    Achievable

    1. Migration: add a nullable image (string, storage path) column to products — non-destructive, matches the pattern of products.cost_price being nullable.
    2. Add FileUpload::make('image')->image()->directory('products')->disk('public') to Modules/Products/Filament/Company/Resources/Products/Schemas/ProductForm.php, and wire the key through ProductService::createProduct()/updateProduct() (both currently whitelist explicit keys — image must be added to both arrays or it will silently be dropped).
    3. Add an image_url accessor on Product wrapping Storage::disk('public')->url($this->image).
    4. Update Modules/Core/resources/Templates/Views/templates/invoices/default.blade.php and .../quotes/default.blade.php to add an image <td> per row, guarding for a null image on either the product or a manually-entered line item with no product_id.
    5. Tests live in Modules/Products/Tests/Feature/ (upload/storage) and Modules/Invoices/Tests/Feature/ + Modules/Quotes/Tests/Feature/ (PDF rendering).

    Relevant

    Product images on customer-facing documents are a common expectation for any catalogue-driven invoicing tool and directly support the sales/professionalism goal in the issue — but today there is no image field on Product at all, so this is new surface area, not a wiring task.

    Time-bound

    Sprint 6 — the issue's own "next sprint" framing is optimistic given the template changes required in two separate Blade views; scope this realistically rather than compressing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    refinedRefined with SMART plan / test suggestions

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions