Skip to content

Commit a194222

Browse files
committed
feat: Implement weekly stock reminder emails for sellers and update the forgot password email subject.
1 parent df23f52 commit a194222

5 files changed

Lines changed: 265 additions & 59 deletions

File tree

app/Console/Kernel.php

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
namespace App\Console;
44

55
use App\Jobs\JobCheckSubscriptionProduct;
6+
use App\Mail\StockReminderMail;
7+
use App\Models\User;
68
use Illuminate\Console\Scheduling\Schedule;
79
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
10+
use Illuminate\Support\Facades\Mail;
811

912
class Kernel extends ConsoleKernel
1013
{
@@ -17,14 +20,67 @@ protected function schedule(Schedule $schedule): void
1720
// $schedule->command('inspire')->hourly();
1821
//$schedule->command('log:clear')->everyTwoMinutes();
1922
$schedule->command('queue:work --stop-when-empty')->everyMinute();
23+
24+
$schedule->call(function () {
25+
// On charge les produits avec leurs variations et les attributs des variations
26+
$sellers = User::where('role', 'seller')
27+
->with(['products.variations.color', 'products.variations.attributesVariation.attributeValue'])
28+
->get();
29+
30+
foreach ($sellers as $seller) {
31+
$stockData = [];
32+
33+
foreach ($seller->products as $product) {
34+
if ($product->variations->isEmpty()) {
35+
// CAS 1 : Produit Simple
36+
$stockData[] = [
37+
'id' => $product->id,
38+
'name' => $product->product_name,
39+
'qty' => $product->product_quantity,
40+
'url' => $product->product_url
41+
];
42+
} else {
43+
// CAS 2 : Produit à Variations
44+
foreach ($product->variations as $variation) {
45+
$colorName = $variation->color->value ?? '';
46+
47+
if ($variation->attributesVariation->isEmpty()) {
48+
// Couleur uniquement
49+
$stockData[] = [
50+
'id' => $product->id,
51+
'name' => "{$product->product_name} ({$colorName})",
52+
'qty' => $variation->quantity,
53+
'url' => $product->product_url
54+
];
55+
} else {
56+
// Couleur + Attributs (Taille, etc.)
57+
foreach ($variation->attributesVariation as $attr) {
58+
$attrValue = $attr->attributeValue->value ?? '';
59+
$stockData[] = [
60+
'id' => $product->id,
61+
'name' => "{$product->product_name} ({$colorName} - {$attrValue})",
62+
'qty' => $attr->quantity,
63+
'url' => $product->product_url
64+
];
65+
}
66+
}
67+
}
68+
}
69+
}
70+
71+
if (!empty($stockData)) {
72+
Mail::to($seller->email)->queue(new StockReminderMail($seller, $stockData));
73+
}
74+
}
75+
})->weeklyOn(0, '15:00');
2076
}
2177

2278
/**
2379
* Register the commands for the application.
2480
*/
2581
protected function commands(): void
2682
{
27-
$this->load(__DIR__.'/Commands');
83+
$this->load(__DIR__ . '/Commands');
2884

2985
require base_path('routes/console.php');
3086
}

app/Mail/ForgotPasswordMail.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public function __construct($otp)
2828
public function envelope(): Envelope
2929
{
3030
return new Envelope(
31-
subject: 'Forgot Password Mail',
31+
subject: 'Mot de passe oublié',
3232
);
3333
}
3434

app/Mail/StockReminderMail.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace App\Mail;
4+
5+
use Illuminate\Bus\Queueable;
6+
use Illuminate\Contracts\Queue\ShouldQueue;
7+
use Illuminate\Mail\Mailable;
8+
use Illuminate\Mail\Mailables\Content;
9+
use Illuminate\Mail\Mailables\Envelope;
10+
use Illuminate\Queue\SerializesModels;
11+
12+
class StockReminderMail extends Mailable
13+
{
14+
use Queueable, SerializesModels;
15+
16+
/**
17+
* Create a new message instance.
18+
*/
19+
public function __construct(public $user, public $products) {}
20+
21+
/**
22+
* Get the message envelope.
23+
*/
24+
public function envelope(): Envelope
25+
{
26+
return new Envelope(
27+
subject: 'Mise à jour hebdomadaire de vos stocks - Akevas',
28+
);
29+
}
30+
31+
/**
32+
* Get the message content definition.
33+
*/
34+
public function content(): Content
35+
{
36+
return new Content(
37+
view: 'stock_reminder',
38+
with: [
39+
'user' => $this->user,
40+
'products' => $this->products,
41+
],
42+
);
43+
}
44+
45+
/**
46+
* Get the attachments for the message.
47+
*
48+
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
49+
*/
50+
public function attachments(): array
51+
{
52+
return [];
53+
}
54+
}

app/Models/Product.php

Lines changed: 62 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -63,79 +63,84 @@ protected static function boot()
6363
$model->id = Uuid::uuid4()->toString();
6464
});
6565
}
66-
public function orderDetails():HasMany{
66+
public function orderDetails(): HasMany
67+
{
6768
return $this->hasMany(OrderDetail::class);
6869
}
69-
public function reviews():HasMany{
70+
public function reviews(): HasMany
71+
{
7072
return $this->hasMany(Review::class);
7173
}
72-
public function productAttributesValues():HasMany{
74+
public function productAttributesValues(): HasMany
75+
{
7376
return $this->hasMany(ProductAttributesValue::class);
7477
}
75-
public function variations(){
78+
public function variations()
79+
{
7680
return $this->hasMany(ProductVariation::class);
7781
}
7882

79-
public function orderVariations(){
83+
public function orderVariations()
84+
{
8085
return $this->hasManyThrough(OrderVariation::class, ProductVariation::class);
8186
}
8287

8388
public function getVariations()
84-
{
85-
// Récupère toutes les variations du produit
86-
return $this->variations->map(function($variation) {
87-
// Vérifie si c'est une variation couleur uniquement
88-
$isColorOnly = collect($variation->attributesVariation)->isEmpty() && $variation->quantity != null;
89-
// Images de la variation couleur
90-
$images = $variation->images->map(function($img) {
91-
return URL("/storage/" . $img->image_path);
92-
});
89+
{
90+
// Récupère toutes les variations du produit
91+
return $this->variations->map(function ($variation) {
92+
// Vérifie si c'est une variation couleur uniquement
93+
$isColorOnly = collect($variation->attributesVariation)->isEmpty() && $variation->quantity != null;
94+
// Images de la variation couleur
95+
$images = $variation->images->map(function ($img) {
96+
return URL("/storage/" . $img->image_path);
97+
});
9398

94-
$base = [
95-
"id" => $variation->id,
96-
"color" => [
97-
"id" => $variation->color->id,
98-
"name" => $variation->color->value,
99-
"hex" => $variation->color->hex_color,
100-
],
101-
"images" => $images,
102-
"isColorOnly" => $isColorOnly,
103-
];
99+
$base = [
100+
"id" => $variation->id,
101+
"color" => [
102+
"id" => $variation->color->id,
103+
"name" => $variation->color->value,
104+
"hex" => $variation->color->hex_color,
105+
],
106+
"images" => $images,
107+
"isColorOnly" => $isColorOnly,
108+
];
104109

105-
if ($isColorOnly) {
106-
// Cas couleur uniquement
107-
$base["quantity"] = $variation->quantity;
108-
$base["price"] = $variation->price;
109-
// Paliers de prix de gros définis au niveau de la variation couleur
110-
111-
} else {
112-
// Cas couleur + attributs (taille/pointure)
113-
$base["attributes"] = $variation->attributesVariation->map(function($attr) {
114-
return [
115-
"id" => $attr->id,
116-
"name" => $attr->attributeValue->attribute->name ?? null,
117-
"value" => $attr->attributeValue->value ?? null,
118-
"group"=>$attr->attributeValue->attributeValueGroup->label ?? null,
119-
"label"=>$attr->attributeValue->label ?? null,
120-
"quantity" => $attr->quantity ?? null,
121-
"price" => $attr->price ?? null,
122-
// Paliers de prix de gros définis au niveau de l'attribut de variation
123-
"wholesale_prices" => $attr->wholesalePrices
124-
->sortBy("min_quantity")
125-
->values()
126-
->map(function($wp) {
127-
return [
128-
"min_quantity" => $wp->min_quantity,
129-
"wholesale_price" => $wp->wholesale_price,
130-
];
131-
}),
132-
];
133-
});
134-
}
110+
if ($isColorOnly) {
111+
// Cas couleur uniquement
112+
$base["quantity"] = $variation->quantity;
113+
$base["price"] = $variation->price;
114+
// Paliers de prix de gros définis au niveau de la variation couleur
135115

136-
return $base;
137-
});
138-
}
116+
} else {
117+
// Cas couleur + attributs (taille/pointure)
118+
$base["attributes"] = $variation->attributesVariation->map(function ($attr) {
119+
return [
120+
"id" => $attr->id,
121+
"name" => $attr->attributeValue->attribute->name ?? null,
122+
"value" => $attr->attributeValue->value ?? null,
123+
"group" => $attr->attributeValue->attributeValueGroup->label ?? null,
124+
"label" => $attr->attributeValue->label ?? null,
125+
"quantity" => $attr->quantity ?? null,
126+
"price" => $attr->price ?? null,
127+
// Paliers de prix de gros définis au niveau de l'attribut de variation
128+
"wholesale_prices" => $attr->wholesalePrices
129+
->sortBy("min_quantity")
130+
->values()
131+
->map(function ($wp) {
132+
return [
133+
"min_quantity" => $wp->min_quantity,
134+
"wholesale_price" => $wp->wholesale_price,
135+
];
136+
}),
137+
];
138+
});
139+
}
140+
141+
return $base;
142+
});
143+
}
139144

140145
public function wholesalePrices()
141146
{
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<style>
7+
.button {
8+
background-color: #ed7e0f;
9+
color: white !important;
10+
padding: 12px 25px;
11+
text-decoration: none;
12+
border-radius: 8px;
13+
font-weight: bold;
14+
display: inline-block;
15+
}
16+
17+
.table {
18+
width: 100%;
19+
border-collapse: collapse;
20+
margin: 20px 0;
21+
}
22+
23+
.table th {
24+
background-color: #f8f9fa;
25+
border-bottom: 2px solid #dee2e6;
26+
padding: 10px;
27+
text-align: left;
28+
font-size: 14px;
29+
}
30+
31+
.table td {
32+
border-bottom: 1px solid #eee;
33+
padding: 10px;
34+
font-size: 14px;
35+
}
36+
37+
.badge-warning {
38+
color: #856404;
39+
background-color: #fff3cd;
40+
padding: 4px 8px;
41+
border-radius: 4px;
42+
font-size: 12px;
43+
}
44+
</style>
45+
</head>
46+
47+
<body style="font-family: 'Segoe UI', Helvetica, Arial, sans-serif; margin: 0; padding: 0; background-color: #f9f9f9;">
48+
<table class="table">
49+
<thead>
50+
<tr>
51+
<th style="width: 50%;">Produit / Variante</th>
52+
<th style="text-align: center;">Stock actuel</th>
53+
<th style="text-align: center;">Action</th>
54+
</tr>
55+
</thead>
56+
<tbody>
57+
@foreach($products as $item)
58+
<tr>
59+
<td style="color: #333; font-weight: 500;">
60+
{{ $item['name'] }}
61+
</td>
62+
<td style="text-align: center;">
63+
<span style="font-size: 16px; font-weight: bold; {{ $item['qty'] <= 3 ? 'color: #d9534f;' : '' }}">
64+
{{ $item['qty'] }}
65+
</span>
66+
@if($item['qty'] <= 3)
67+
<div style="font-size: 10px; color: #d9534f; font-weight: bold; text-transform: uppercase;">
68+
Critique
69+
</div>
70+
@endif
71+
</td>
72+
<td style="text-align: center;">
73+
@if(isset($item['url']))
74+
<a href="https://seller.akevas.com/seller/edit/product/{{ $item['url'] }}" class="button" style="padding: 6px 12px; font-size: 12px;">
75+
Voir
76+
</a>
77+
@endif
78+
</td>
79+
</tr>
80+
@endforeach
81+
</tbody>
82+
</table>
83+
84+
<div style="text-align: center; margin: 30px 0;">
85+
<a href="https://seller.akevas.com/seller/products" class="button">
86+
Voir tous mes produits
87+
</a>
88+
</div>
89+
</body>
90+
91+
</html>

0 commit comments

Comments
 (0)