You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Products]: Allow the user to upload/insert a product image that will be shown in quotes and invoices. #63
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.
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.
Migration: add a nullable image (string, storage path) column to products — non-destructive, matches the pattern of products.cost_price being nullable.
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).
Add an image_url accessor on Product wrapping Storage::disk('public')->url($this->image).
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.
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.
Activity
nielsdrost7 commentedon Jan 2, 2026
@coderabbitai make pull-request with AAA test Scenario's: Arrange, Act, Assert. And also make sure you test edge cases
coderabbitai commentedon Jan 2, 2026
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.
nielsdrost7 commentedon Jul 16, 2026
SMART Plan — Product images on quotes and invoices
Specific
Let a company upload an image per product and have it appear automatically on generated quotes and invoices. Current state:
Modules\Products\Models\Producthas no image field or upload relationship at all — a repo-wide search forimage/photoinsideModules/Productsreturns nothing. Two existing building blocks could support this: (1)Modules\Core\Models\Upload— a generic polymorphic model (uploadable_type/uploadable_idviamorphTo(), company-scoped) that is fully defined but currently used by zero models anywhere in the codebase (nomorphMany(Upload::class, 'uploadable')exists on any model today), or (2) the simpler precedent already used for company branding —Modules/Core/Filament/Admin/Pages/Settings.phpuses plainFileUpload::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 simplerFileUpload-on-the-model pattern is the better fit than standing up the unused genericUploadpolymorphic 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
image(storage path) column, editable via aFileUploadfield on the product form.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); }#[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); }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); }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); }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
image(string, storage path) column toproducts— non-destructive, matches the pattern ofproducts.cost_pricebeing nullable.FileUpload::make('image')->image()->directory('products')->disk('public')toModules/Products/Filament/Company/Resources/Products/Schemas/ProductForm.php, and wire the key throughProductService::createProduct()/updateProduct()(both currently whitelist explicit keys —imagemust be added to both arrays or it will silently be dropped).image_urlaccessor onProductwrappingStorage::disk('public')->url($this->image).Modules/Core/resources/Templates/Views/templates/invoices/default.blade.phpand.../quotes/default.blade.phpto add an image<td>per row, guarding for a null image on either the product or a manually-entered line item with noproduct_id.Modules/Products/Tests/Feature/(upload/storage) andModules/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
Productat 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.