Skip to content

[Invoices] Email action opens modal pre-filled with recipient, subject, and body from email template #522

Closed

Description

@nielsdrost7

Sub-task extracted from #169 during consolidation into #150.

Parent: #150

Clicking the "Email Invoice" action should open a modal pre-filled with:

  • Recipient (customer.email)
  • Subject (configurable default, e.g. Invoice #123)
  • Body (from the company's invoice email template)

Activity

  1. added this to the v2.0.0-alpha milestone on Jul 3, 2026
  2. KhawarMehfooz commented on Jul 16, 2026

    @KhawarMehfooz
    Collaborator

    @nielsdrost7 while working on this issue I found that there is no email field for customer. Is it implemented?

    While creating a relation, there is no field to enter customer email, only customer name is entered.

  3. nielsdrost7 commented on Jul 16, 2026

    @nielsdrost7
    CollaboratorAuthor

    It's in the communicable pivot table, just Like where you put those fields for the CC email addresses.

    Unless it gets too complicated, but otherwise you'll have 1 email address field in the Relations table and dozens in the communicable table, but also the email address from Relations can be the same as the ones in communicable.

    Hope it makes sense to stuff all those fields in anything related to communications: phone, email, whatsapl into that single table

  4. nielsdrost7 commented on Jul 16, 2026

    @nielsdrost7
    CollaboratorAuthor

    SMART Plan — Email action opens modal pre-filled with recipient, subject, and body

    Context/Epic: Core — sub-task of #150 (Invoices epic #15)

    Specific

    Give the invoice list's "Send Email" action a pre-filled modal instead of firing immediately. Today, Action::make('send email') in InvoicesTable (Modules/Invoices/Filament/Company/Resources/Invoices/Tables/InvoicesTable.php:117-128) has no form/modal at all — it calls app(InvoiceService::class)->sendInvoiceEmail($record) directly from ->action(), and that method does not exist anywhere in InvoiceService or any other class in the codebase (confirmed via a codebase-wide grep). Clicking "Send Email" today throws a fatal Error: Call to undefined method Modules\Invoices\Services\InvoiceService::sendInvoiceEmail(). Separately: the recipient can't simply be $record->customer->email as the parent issue (#150) and its sibling sub-tasks assume — Relation (the customer model) has no email column; Relation::getCustomerEmailAttribute() reads $this->email, which isn't backed by any column, so it always resolves to null. A client's email actually lives in the polymorphic Communication model (Relation::communications(): MorphMany, communication_type = 'email'). This sub-task's pre-fill logic needs to resolve through that relation, not a direct attribute.

    Measurable

    • The "send email" action gains a form/schema with to, subject, and body fields (currently has none).
      • Frontend/Livewire: As a company admin, clicking "Send Email" on an invoice should open a modal instead of sending immediately, so I can review before it goes out.
        #[Test]
        public function it_opens_a_modal_with_recipient_subject_and_body_fields_when_sending_an_invoice_email(): void
        {
            /* Arrange */
            $client  = Relation::factory()->for($this->company)->create();
            $client->communications()->create([
                'company_id' => $this->company->id, 'communication_type' => CommunicationType::EMAIL->value,
                'communication_value' => 'client@example.com', 'is_primary' => true,
            ]);
            $invoice = Invoice::factory()->for($this->company)->create(['customer_id' => $client->id]);
    
            /* Act */
            $component = $this->testLivewire(ListInvoices::class)
                ->mountTableAction('send email', $invoice);
    
            /* Assert */
            $component->assertSchemaStateSet(['to' => 'client@example.com']);
        }
    • The subject is pre-filled from a configurable default (e.g. Invoice #{number}), not left blank.
      • PHPUnit: it_prefills_the_subject_with_the_invoice_number()
        #[Test]
        public function it_prefills_the_subject_with_the_invoice_number(): void
        {
            /* Arrange */
            $invoice = Invoice::factory()->for($this->company)->create(['invoice_number' => 'INV-2026-001']);
    
            /* Act */
            $component = $this->testLivewire(ListInvoices::class)
                ->mountTableAction('send email', $invoice);
    
            /* Assert */
            $component->assertSchemaStateSet(['subject' => 'Invoice #INV-2026-001']);
        }
    • The body is pre-filled from the company's invoice EmailTemplate, when one exists.
      • PHPUnit: it_prefills_the_body_from_the_companys_invoice_email_template()
        #[Test]
        public function it_prefills_the_body_from_the_companys_invoice_email_template(): void
        {
            /* Arrange */
            EmailTemplate::factory()->for($this->company)->create(['title' => 'Invoice Default', 'body' => 'Please find your invoice attached.']);
            $invoice = Invoice::factory()->for($this->company)->create();
    
            /* Act */
            $component = $this->testLivewire(ListInvoices::class)
                ->mountTableAction('send email', $invoice);
    
            /* Assert */
            $component->assertSchemaStateSet(['body' => 'Please find your invoice attached.']);
        }
    • When the client has no resolvable email, the recipient field is pre-filled empty rather than the action failing outright.
      • PHPUnit: it_leaves_the_recipient_blank_when_the_client_has_no_email_on_file()
        #[Test]
        public function it_leaves_the_recipient_blank_when_the_client_has_no_email_on_file(): void
        {
            /* Arrange */
            $client  = Relation::factory()->for($this->company)->create();
            $invoice = Invoice::factory()->for($this->company)->create(['customer_id' => $client->id]);
    
            /* Act */
            $component = $this->testLivewire(ListInvoices::class)
                ->mountTableAction('send email', $invoice);
    
            /* Assert */
            $component->assertSchemaStateSet(['to' => '']);
        }

    Achievable

    1. Add a primary_email-style resolver to Relation that reads through communications() (where('communication_type', CommunicationType::EMAIL->value)->where('is_primary', true)), since no existing code path returns a plain email string for a client today.
    2. Replace the current no-form Action::make('send email') with a form-carrying Action (->schema([TextInput::make('to'), TextInput::make('subject'), RichEditor/Textarea::make('body')])) in InvoicesTable.
    3. Pre-fill via ->fillForm(fn (Invoice $record) => [...]), resolving to from the new Relation accessor, subject from a configurable default string, and body from the company's EmailTemplate (matched on title for now, since EmailTemplate has no invoice-specific trigger/type field yet — see [Invoices]: Email Template Tags — add payment method variable #366's grounding on this same gap).
    4. Do not wire the actual send in this sub-task — that's [Invoices] Allow user to edit recipient, subject, and body before sending the invoice email #524 (editing) and [Invoices] Attach invoice PDF automatically when sending an invoice email #526/[Invoices] Create/update MailQueue record to log email delivery (timestamp, recipient, status) #527/[Invoices] Change invoice status to "Sent" when emailed from Draft #529/[Invoices] Handle invoice email send failures gracefully — show error notification, no status change #531 (attach/log/status/errors); this slice only needs the modal to open pre-filled.
    5. Tests live in Modules/Invoices/Tests/Feature/InvoiceEmailModalTest.php (new file).

    Relevant

    This is the first slice of #150 (the epic's "implement the send-email stub" sub-issue) — without a modal, the user has no chance to review recipient/subject/body before anything is sent, and today the action doesn't even run without erroring.

    Time-bound

    Sprint 6 — first of the six #150 sub-tasks in this batch; #524 depends on this modal existing.

  5. added a commit that references this issue on Jul 16, 2026
    b0e9766
  6. added a commit that references this issue on Aug 17, 2026
    aaa3026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

high-priorefinedRefined with SMART plan / test suggestions

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions