Compare commits

...

6 Commits

14 changed files with 585 additions and 328 deletions

37
app/Mail/InvoiceMail.php Normal file
View File

@ -0,0 +1,37 @@
<?php
namespace App\Mail;
use App\Models\Invoice;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class InvoiceMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public Invoice $invoice
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: "Invoice {$this->invoice->invoice_number} - Payment Requested",
);
}
public function content(): Content
{
return new Content(
view: 'emails.invoice',
with: [
'invoice' => $this->invoice,
'invoiceUrl' => route('invoices.show', $this->invoice),
],
);
}
}

View File

@ -10,10 +10,12 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\SoftDeletes;
class Client extends Model class Client extends Model
{ {
use HasFactory; use HasFactory;
use SoftDeletes;
protected $fillable = [ protected $fillable = [
'name', 'name',

View File

@ -8,10 +8,12 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Contact extends Model class Contact extends Model
{ {
use HasFactory; use HasFactory;
use SoftDeletes;
public $fillable = ['first_name', 'last_name', 'email', 'phone']; public $fillable = ['first_name', 'last_name', 'email', 'phone'];

View File

@ -1,5 +1,4 @@
{ {
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/livewire-starter-kit", "name": "laravel/livewire-starter-kit",
"type": "project", "type": "project",
"description": "The official Laravel starter kit for Livewire.", "description": "The official Laravel starter kit for Livewire.",
@ -19,6 +18,8 @@
"symfony/clock": "^7.0", "symfony/clock": "^7.0",
"symfony/css-selector": "^7.0", "symfony/css-selector": "^7.0",
"symfony/event-dispatcher": "^7.0", "symfony/event-dispatcher": "^7.0",
"symfony/http-client": "^7.0",
"symfony/mailgun-mailer": "^7.0",
"symfony/string": "^7.0", "symfony/string": "^7.0",
"symfony/translation": "^7.0" "symfony/translation": "^7.0"
}, },

386
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "a8cbc03ab467c1313282b2c05b78def7", "content-hash": "e9c7f68a4a154c90522fc7d615f86f69",
"packages": [ "packages": [
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@ -63,16 +63,16 @@
}, },
{ {
"name": "brick/math", "name": "brick/math",
"version": "0.14.1", "version": "0.14.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/brick/math.git", "url": "https://github.com/brick/math.git",
"reference": "f05858549e5f9d7bb45875a75583240a38a281d0" "reference": "55c950aa71a2cabc1d8f2bec1f8a7020bd244aa2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", "url": "https://api.github.com/repos/brick/math/zipball/55c950aa71a2cabc1d8f2bec1f8a7020bd244aa2",
"reference": "f05858549e5f9d7bb45875a75583240a38a281d0", "reference": "55c950aa71a2cabc1d8f2bec1f8a7020bd244aa2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -111,7 +111,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/brick/math/issues", "issues": "https://github.com/brick/math/issues",
"source": "https://github.com/brick/math/tree/0.14.1" "source": "https://github.com/brick/math/tree/0.14.2"
}, },
"funding": [ "funding": [
{ {
@ -119,7 +119,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-11-24T14:40:29+00:00" "time": "2026-01-30T14:03:11+00:00"
}, },
{ {
"name": "carbonphp/carbon-doctrine-types", "name": "carbonphp/carbon-doctrine-types",
@ -2434,16 +2434,16 @@
}, },
{ {
"name": "nesbot/carbon", "name": "nesbot/carbon",
"version": "3.11.0", "version": "3.11.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/CarbonPHP/carbon.git", "url": "https://github.com/CarbonPHP/carbon.git",
"reference": "bdb375400dcd162624531666db4799b36b64e4a1" "reference": "f438fcc98f92babee98381d399c65336f3a3827f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1", "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/f438fcc98f92babee98381d399c65336f3a3827f",
"reference": "bdb375400dcd162624531666db4799b36b64e4a1", "reference": "f438fcc98f92babee98381d399c65336f3a3827f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2467,7 +2467,7 @@
"phpstan/extension-installer": "^1.4.3", "phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.1.22", "phpstan/phpstan": "^2.1.22",
"phpunit/phpunit": "^10.5.53", "phpunit/phpunit": "^10.5.53",
"squizlabs/php_codesniffer": "^3.13.4" "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0"
}, },
"bin": [ "bin": [
"bin/carbon" "bin/carbon"
@ -2510,14 +2510,14 @@
} }
], ],
"description": "An API extension for DateTime that supports 281 different languages.", "description": "An API extension for DateTime that supports 281 different languages.",
"homepage": "https://carbon.nesbot.com", "homepage": "https://carbonphp.github.io/carbon/",
"keywords": [ "keywords": [
"date", "date",
"datetime", "datetime",
"time" "time"
], ],
"support": { "support": {
"docs": "https://carbon.nesbot.com/docs", "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html",
"issues": "https://github.com/CarbonPHP/carbon/issues", "issues": "https://github.com/CarbonPHP/carbon/issues",
"source": "https://github.com/CarbonPHP/carbon" "source": "https://github.com/CarbonPHP/carbon"
}, },
@ -2535,7 +2535,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-02T21:04:28+00:00" "time": "2026-01-29T09:26:29+00:00"
}, },
{ {
"name": "nette/schema", "name": "nette/schema",
@ -3446,16 +3446,16 @@
}, },
{ {
"name": "psy/psysh", "name": "psy/psysh",
"version": "v0.12.18", "version": "v0.12.19",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/bobthecow/psysh.git", "url": "https://github.com/bobthecow/psysh.git",
"reference": "ddff0ac01beddc251786fe70367cd8bbdb258196" "reference": "a4f766e5c5b6773d8399711019bb7d90875a50ee"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/ddff0ac01beddc251786fe70367cd8bbdb258196", "url": "https://api.github.com/repos/bobthecow/psysh/zipball/a4f766e5c5b6773d8399711019bb7d90875a50ee",
"reference": "ddff0ac01beddc251786fe70367cd8bbdb258196", "reference": "a4f766e5c5b6773d8399711019bb7d90875a50ee",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3519,9 +3519,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/bobthecow/psysh/issues", "issues": "https://github.com/bobthecow/psysh/issues",
"source": "https://github.com/bobthecow/psysh/tree/v0.12.18" "source": "https://github.com/bobthecow/psysh/tree/v0.12.19"
}, },
"time": "2025-12-17T14:35:46+00:00" "time": "2026-01-30T17:33:13+00:00"
}, },
{ {
"name": "ralouphie/getallheaders", "name": "ralouphie/getallheaders",
@ -4337,16 +4337,16 @@
}, },
{ {
"name": "symfony/finder", "name": "symfony/finder",
"version": "v7.4.4", "version": "v7.4.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/finder.git", "url": "https://github.com/symfony/finder.git",
"reference": "01b24a145bbeaa7141e75887ec904c34a6728a5f" "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/01b24a145bbeaa7141e75887ec904c34a6728a5f", "url": "https://api.github.com/repos/symfony/finder/zipball/ad4daa7c38668dcb031e63bc99ea9bd42196a2cb",
"reference": "01b24a145bbeaa7141e75887ec904c34a6728a5f", "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4381,7 +4381,7 @@
"description": "Finds files and directories via an intuitive fluent interface", "description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/finder/tree/v7.4.4" "source": "https://github.com/symfony/finder/tree/v7.4.5"
}, },
"funding": [ "funding": [
{ {
@ -4401,20 +4401,199 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-01-12T12:19:02+00:00" "time": "2026-01-26T15:07:59+00:00"
}, },
{ {
"name": "symfony/http-foundation", "name": "symfony/http-client",
"version": "v7.4.4", "version": "v7.4.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/http-foundation.git", "url": "https://github.com/symfony/http-client.git",
"reference": "977a554a34cf8edc95ca351fbecb1bb1ad05cc94" "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/977a554a34cf8edc95ca351fbecb1bb1ad05cc94", "url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f",
"reference": "977a554a34cf8edc95ca351fbecb1bb1ad05cc94", "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/log": "^1|^2|^3",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/http-client-contracts": "~3.4.4|^3.5.2",
"symfony/polyfill-php83": "^1.29",
"symfony/service-contracts": "^2.5|^3"
},
"conflict": {
"amphp/amp": "<2.5",
"amphp/socket": "<1.1",
"php-http/discovery": "<1.15",
"symfony/http-foundation": "<6.4"
},
"provide": {
"php-http/async-client-implementation": "*",
"php-http/client-implementation": "*",
"psr/http-client-implementation": "1.0",
"symfony/http-client-implementation": "3.0"
},
"require-dev": {
"amphp/http-client": "^4.2.1|^5.0",
"amphp/http-tunnel": "^1.0|^2.0",
"guzzlehttp/promises": "^1.4|^2.0",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"psr/http-client": "^1.0",
"symfony/amphp-http-client-meta": "^1.0|^2.0",
"symfony/cache": "^6.4|^7.0|^8.0",
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/http-kernel": "^6.4|^7.0|^8.0",
"symfony/messenger": "^6.4|^7.0|^8.0",
"symfony/process": "^6.4|^7.0|^8.0",
"symfony/rate-limiter": "^6.4|^7.0|^8.0",
"symfony/stopwatch": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
"homepage": "https://symfony.com",
"keywords": [
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.4.5"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-01-27T16:16:02+00:00"
},
{
"name": "symfony/http-client-contracts",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client-contracts.git",
"reference": "75d7043853a42837e68111812f4d964b01e5101c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c",
"reference": "75d7043853a42837e68111812f4d964b01e5101c",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.6-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Test/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to HTTP clients",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-04-29T11:18:49+00:00"
},
{
"name": "symfony/http-foundation",
"version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "446d0db2b1f21575f1284b74533e425096abdfb6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/446d0db2b1f21575f1284b74533e425096abdfb6",
"reference": "446d0db2b1f21575f1284b74533e425096abdfb6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4463,7 +4642,7 @@
"description": "Defines an object-oriented layer for the HTTP specification", "description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/http-foundation/tree/v7.4.4" "source": "https://github.com/symfony/http-foundation/tree/v7.4.5"
}, },
"funding": [ "funding": [
{ {
@ -4483,20 +4662,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-01-09T12:14:21+00:00" "time": "2026-01-27T16:16:02+00:00"
}, },
{ {
"name": "symfony/http-kernel", "name": "symfony/http-kernel",
"version": "v7.4.4", "version": "v7.4.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/http-kernel.git", "url": "https://github.com/symfony/http-kernel.git",
"reference": "48b067768859f7b68acf41dfb857a5a4be00acdd" "reference": "229eda477017f92bd2ce7615d06222ec0c19e82a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/48b067768859f7b68acf41dfb857a5a4be00acdd", "url": "https://api.github.com/repos/symfony/http-kernel/zipball/229eda477017f92bd2ce7615d06222ec0c19e82a",
"reference": "48b067768859f7b68acf41dfb857a5a4be00acdd", "reference": "229eda477017f92bd2ce7615d06222ec0c19e82a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4582,7 +4761,7 @@
"description": "Provides a structured process for converting a Request into a Response", "description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/http-kernel/tree/v7.4.4" "source": "https://github.com/symfony/http-kernel/tree/v7.4.5"
}, },
"funding": [ "funding": [
{ {
@ -4602,7 +4781,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-01-24T22:13:01+00:00" "time": "2026-01-28T10:33:42+00:00"
}, },
{ {
"name": "symfony/mailer", "name": "symfony/mailer",
@ -4689,17 +4868,90 @@
"time": "2026-01-08T08:25:11+00:00" "time": "2026-01-08T08:25:11+00:00"
}, },
{ {
"name": "symfony/mime", "name": "symfony/mailgun-mailer",
"version": "v7.4.4", "version": "v7.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/mime.git", "url": "https://github.com/symfony/mailgun-mailer.git",
"reference": "40945014c0a9471ccfe19673c54738fa19367a3c" "reference": "ffbcdbf93ed0700f083a6307acfb8f78dd3f091b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/40945014c0a9471ccfe19673c54738fa19367a3c", "url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/ffbcdbf93ed0700f083a6307acfb8f78dd3f091b",
"reference": "40945014c0a9471ccfe19673c54738fa19367a3c", "reference": "ffbcdbf93ed0700f083a6307acfb8f78dd3f091b",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/mailer": "^7.2|^8.0"
},
"conflict": {
"symfony/http-foundation": "<6.4"
},
"require-dev": {
"symfony/http-client": "^6.4|^7.0|^8.0",
"symfony/webhook": "^6.4|^7.0|^8.0"
},
"type": "symfony-mailer-bridge",
"autoload": {
"psr-4": {
"Symfony\\Component\\Mailer\\Bridge\\Mailgun\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Mailgun Mailer Bridge",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/mailgun-mailer/tree/v7.4.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-08-04T07:05:15+00:00"
},
{
"name": "symfony/mime",
"version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "b18c7e6e9eee1e19958138df10412f3c4c316148"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/b18c7e6e9eee1e19958138df10412f3c4c316148",
"reference": "b18c7e6e9eee1e19958138df10412f3c4c316148",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4710,15 +4962,15 @@
}, },
"conflict": { "conflict": {
"egulias/email-validator": "~3.0.0", "egulias/email-validator": "~3.0.0",
"phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/reflection-docblock": "<5.2|>=6",
"phpdocumentor/type-resolver": "<1.4.0", "phpdocumentor/type-resolver": "<1.5.1",
"symfony/mailer": "<6.4", "symfony/mailer": "<6.4",
"symfony/serializer": "<6.4.3|>7.0,<7.0.3" "symfony/serializer": "<6.4.3|>7.0,<7.0.3"
}, },
"require-dev": { "require-dev": {
"egulias/email-validator": "^2.1.10|^3.1|^4", "egulias/email-validator": "^2.1.10|^3.1|^4",
"league/html-to-markdown": "^5.0", "league/html-to-markdown": "^5.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", "phpdocumentor/reflection-docblock": "^5.2",
"symfony/dependency-injection": "^6.4|^7.0|^8.0", "symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/process": "^6.4|^7.0|^8.0", "symfony/process": "^6.4|^7.0|^8.0",
"symfony/property-access": "^6.4|^7.0|^8.0", "symfony/property-access": "^6.4|^7.0|^8.0",
@ -4755,7 +5007,7 @@
"mime-type" "mime-type"
], ],
"support": { "support": {
"source": "https://github.com/symfony/mime/tree/v7.4.4" "source": "https://github.com/symfony/mime/tree/v7.4.5"
}, },
"funding": [ "funding": [
{ {
@ -4775,7 +5027,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-01-08T16:12:55+00:00" "time": "2026-01-27T08:59:58+00:00"
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
@ -5608,16 +5860,16 @@
}, },
{ {
"name": "symfony/process", "name": "symfony/process",
"version": "v7.4.4", "version": "v7.4.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/process.git", "url": "https://github.com/symfony/process.git",
"reference": "626f07a53f4b4e2f00e11824cc29f928d797783b" "reference": "608476f4604102976d687c483ac63a79ba18cc97"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/626f07a53f4b4e2f00e11824cc29f928d797783b", "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97",
"reference": "626f07a53f4b4e2f00e11824cc29f928d797783b", "reference": "608476f4604102976d687c483ac63a79ba18cc97",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -5649,7 +5901,7 @@
"description": "Executes commands in sub-processes", "description": "Executes commands in sub-processes",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/process/tree/v7.4.4" "source": "https://github.com/symfony/process/tree/v7.4.5"
}, },
"funding": [ "funding": [
{ {
@ -5669,7 +5921,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-01-20T09:23:51+00:00" "time": "2026-01-26T15:07:59+00:00"
}, },
{ {
"name": "symfony/routing", "name": "symfony/routing",
@ -11225,24 +11477,24 @@
}, },
{ {
"name": "ta-tikoma/phpunit-architecture-test", "name": "ta-tikoma/phpunit-architecture-test",
"version": "0.8.5", "version": "0.8.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git",
"reference": "cf6fb197b676ba716837c886baca842e4db29005" "reference": "ad48430b92901fd7d003fdaf2d7b139f96c0906e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/cf6fb197b676ba716837c886baca842e4db29005", "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/ad48430b92901fd7d003fdaf2d7b139f96c0906e",
"reference": "cf6fb197b676ba716837c886baca842e4db29005", "reference": "ad48430b92901fd7d003fdaf2d7b139f96c0906e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"nikic/php-parser": "^4.18.0 || ^5.0.0", "nikic/php-parser": "^4.18.0 || ^5.0.0",
"php": "^8.1.0", "php": "^8.1.0",
"phpdocumentor/reflection-docblock": "^5.3.0", "phpdocumentor/reflection-docblock": "^5.3.0 || ^6.0.0",
"phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0", "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0",
"symfony/finder": "^6.4.0 || ^7.0.0" "symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0"
}, },
"require-dev": { "require-dev": {
"laravel/pint": "^1.13.7", "laravel/pint": "^1.13.7",
@ -11278,9 +11530,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues",
"source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.5" "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.6"
}, },
"time": "2025-04-20T20:23:40+00:00" "time": "2026-01-30T07:16:00+00:00"
}, },
{ {
"name": "theseer/tokenizer", "name": "theseer/tokenizer",

View File

@ -144,7 +144,7 @@ return [
*/ */
'features' => [ 'features' => [
Features::registration(), #Features::registration(),
Features::resetPasswords(), Features::resetPasswords(),
Features::emailVerification(), Features::emailVerification(),
Features::twoFactorAuthentication([ Features::twoFactorAuthentication([

View File

@ -37,6 +37,10 @@ return [
'mailers' => [ 'mailers' => [
'mailgun' => [
'transport' => 'mailgun',
],
'smtp' => [ 'smtp' => [
'transport' => 'smtp', 'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'), 'scheme' => env('MAIL_SCHEME'),

View File

@ -40,4 +40,11 @@ return [
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'), 'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
], ],
'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
'scheme' => 'https',
],
]; ];

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('clients', function (Blueprint $table) {
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('clients', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('contacts', function (Blueprint $table) {
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('contacts', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
};

View File

@ -24,7 +24,7 @@ new class extends Component {
{ {
$this->validate(); $this->validate();
Invoice::create([ $invoice = Invoice::create([
'client_id' => $this->client_id, 'client_id' => $this->client_id,
'status' => $this->status, 'status' => $this->status,
'notes' => $this->notes, 'notes' => $this->notes,
@ -34,6 +34,8 @@ new class extends Component {
$this->reset(); $this->reset();
Flux::modal('create-invoice')->close(); Flux::modal('create-invoice')->close();
$this->dispatch('invoice-created'); $this->dispatch('invoice-created');
$this->redirect(route('invoices.edit', $invoice), navigate: true);
} }
#[Computed] #[Computed]

View File

@ -1,12 +1,14 @@
<?php <?php
use App\Enums\InvoiceStatus; use App\Enums\InvoiceStatus;
use App\Mail\InvoiceMail;
use App\Models\Client; use App\Models\Client;
use App\Models\Invoice; use App\Models\Invoice;
use Illuminate\Support\Facades\Mail;
use Livewire\Component; use Livewire\Component;
use Livewire\Attributes\Computed; use Livewire\Attributes\Computed;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Flux\Flux;
new class extends Component { new class extends Component {
public $invoice; public $invoice;
@ -72,6 +74,56 @@ new class extends Component {
$this->dispatch('invoice-status-changed'); $this->dispatch('invoice-status-changed');
} }
public function sendToPrimaryContact(): void
{
$primaryContact = $this->invoice->client->primary_contact;
if (!$primaryContact || !$primaryContact->email) {
Flux::toast(
text: 'No primary contact with email address found.',
variant: 'danger',
);
return;
}
Mail::to($primaryContact->email)->send(new InvoiceMail($this->invoice));
if ($this->invoice->sent_at === null) {
$this->invoice->update(['sent_at' => now()]);
}
Flux::toast(
text: "Invoice sent to {$primaryContact->full_name}.",
variant: 'success',
);
}
public function sendToAllContacts(): void
{
$contacts = $this->invoice->client->contacts->filter(fn($c) => $c->email);
if ($contacts->isEmpty()) {
Flux::toast(
text: 'No contacts with email addresses found.',
variant: 'danger',
);
return;
}
foreach ($contacts as $contact) {
Mail::to($contact->email)->send(new InvoiceMail($this->invoice));
}
if ($this->invoice->sent_at === null) {
$this->invoice->update(['sent_at' => now()]);
}
Flux::toast(
text: "Invoice sent to {$contacts->count()} contact(s).",
variant: 'success',
);
}
#[Computed] #[Computed]
public function clients() public function clients()
{ {
@ -90,6 +142,8 @@ new class extends Component {
@elseif($this->invoice->status === InvoiceStatus::POSTED) @elseif($this->invoice->status === InvoiceStatus::POSTED)
<flux:button variant="primary" color="red" wire:click="setStatus('void')">Void Invoice</flux:button> <flux:button variant="primary" color="red" wire:click="setStatus('void')">Void Invoice</flux:button>
<flux:button variant="primary" color="amber" wire:click="setStatus('draft')">Un-Post Invoice</flux:button> <flux:button variant="primary" color="amber" wire:click="setStatus('draft')">Un-Post Invoice</flux:button>
<flux:button variant="primary" wire:click="sendToPrimaryContact" wire:loading.attr="disabled">Send to Primary Contact</flux:button>
<flux:button variant="primary" wire:click="sendToAllContacts" wire:loading.attr="disabled">Send to All Contacts</flux:button>
@elseif($this->invoice->status === InvoiceStatus::VOID) @elseif($this->invoice->status === InvoiceStatus::VOID)
<flux:button variant="primary" color="blue" wire:click="setStatus('draft')">Restore Invoice</flux:button> <flux:button variant="primary" color="blue" wire:click="setStatus('draft')">Restore Invoice</flux:button>
@endif @endif

View File

@ -0,0 +1,93 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice {{ $invoice->invoice_number }}</title>
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 8px;">
<h1 style="color: #1a1a1a; margin-top: 0; font-size: 24px;">Invoice {{ $invoice->invoice_number }}</h1>
<p style="margin-bottom: 20px;">Dear {{ $invoice->client->primary_contact?->first_name ?? 'Valued Customer' }},</p>
<p>Please find below a summary of your invoice from eBandroom.</p>
<div style="background-color: #ffffff; padding: 20px; border-radius: 6px; margin: 20px 0; border: 1px solid #e9ecef;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; color: #666;">Invoice Number:</td>
<td style="padding: 8px 0; text-align: right; font-weight: 600;">{{ $invoice->invoice_number }}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;">Invoice Date:</td>
<td style="padding: 8px 0; text-align: right;">{{ $invoice->invoice_date?->format('F j, Y') }}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;">Due Date:</td>
<td style="padding: 8px 0; text-align: right;">{{ $invoice->due_date?->format('F j, Y') }}</td>
</tr>
</table>
<hr style="border: none; border-top: 1px solid #e9ecef; margin: 15px 0;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="border-bottom: 1px solid #e9ecef;">
<th style="padding: 10px 0; text-align: left; color: #666; font-weight: 500;">Description</th>
<th style="padding: 10px 0; text-align: right; color: #666; font-weight: 500;">Amount</th>
</tr>
</thead>
<tbody>
@foreach($invoice->lines as $line)
<tr>
<td style="padding: 10px 0;">
{{ $line->product?->name ?? $line->description }}
@if($line->description && $line->product)
<br><span style="color: #666; font-size: 14px;">{{ $line->description }}</span>
@endif
</td>
<td style="padding: 10px 0; text-align: right;">${{ number_format($line->amount, 2) }}</td>
</tr>
@endforeach
</tbody>
</table>
<hr style="border: none; border-top: 2px solid #e9ecef; margin: 15px 0;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; font-weight: 600; font-size: 18px;">Total Due:</td>
<td style="padding: 8px 0; text-align: right; font-weight: 600; font-size: 18px; color: #2563eb;">${{ number_format($invoice->balance_due, 2) }}</td>
</tr>
</table>
</div>
@if($invoice->notes)
<div style="background-color: #fff3cd; padding: 15px; border-radius: 6px; margin: 20px 0; border-left: 4px solid #ffc107;">
<strong>Notes:</strong><br>
{{ $invoice->notes }}
</div>
@endif
<p style="margin: 25px 0;">To view your complete invoice or pay online, please click the button below:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ $invoiceUrl }}" style="display: inline-block; background-color: #2563eb; color: #ffffff; padding: 14px 28px; text-decoration: none; border-radius: 6px; font-weight: 600;">View Invoice & Pay Online</a>
</div>
<p style="color: #666; font-size: 14px; margin-top: 30px;">
If you have any questions about this invoice, please don't hesitate to contact us.
</p>
<p style="margin-bottom: 0;">
Thank you for your business,<br>
<strong>{{ config('app.name') }}</strong>
</p>
</div>
<div style="text-align: center; padding: 20px; color: #999; font-size: 12px;">
<p>This email was sent regarding invoice {{ $invoice->invoice_number }}.</p>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long