Compare commits
No commits in common. "94400988153459d589febc8fe47ad14c6826ae42" and "9951ed0ee6b74a0ba0f0960c91fb57603d382c92" have entirely different histories.
9440098815
...
9951ed0ee6
@ -1,440 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Http\Requests\ReceiveWebmailMessageRequest;
|
|
||||||
use App\Http\Requests\SendWebmailMessageRequest;
|
|
||||||
use App\Http\Requests\UpsertUserMailboxSettingRequest;
|
|
||||||
use App\Http\Requests\UpdateWebmailMessageRequest;
|
|
||||||
use App\Http\Resources\Webmail\UserMailboxSettingResource;
|
|
||||||
use App\Http\Resources\Webmail\WebmailMessageResource;
|
|
||||||
use App\Models\UserMailboxSetting;
|
|
||||||
use App\Models\WebmailMessage;
|
|
||||||
use App\Repositories\WebmailMessageRepositoryInterface;
|
|
||||||
use App\Services\WebmailService;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class WebmailController extends Controller
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly WebmailMessageRepositoryInterface $webmailRepository,
|
|
||||||
private readonly WebmailService $webmailService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function index(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
if (! $user) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Utilisateur non authentifie.',
|
|
||||||
], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$messages = $this->webmailRepository->paginateForUser(
|
|
||||||
(int) $user->id,
|
|
||||||
[
|
|
||||||
'folder' => $request->query('folder'),
|
|
||||||
'status' => $request->query('status'),
|
|
||||||
'search' => $request->query('search'),
|
|
||||||
'unread' => $request->has('unread') ? $request->boolean('unread') : null,
|
|
||||||
'starred' => $request->has('starred') ? $request->boolean('starred') : null,
|
|
||||||
],
|
|
||||||
max(1, (int) $request->integer('per_page', 15)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => $messages->getCollection()
|
|
||||||
->map(fn (WebmailMessage $message): array => (new WebmailMessageResource($message))->resolve())
|
|
||||||
->values(),
|
|
||||||
'meta' => [
|
|
||||||
'current_page' => $messages->currentPage(),
|
|
||||||
'last_page' => $messages->lastPage(),
|
|
||||||
'per_page' => $messages->perPage(),
|
|
||||||
'total' => $messages->total(),
|
|
||||||
],
|
|
||||||
'message' => 'Messages recuperes avec succes.',
|
|
||||||
]);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('Error fetching webmail messages: ' . $e->getMessage(), [
|
|
||||||
'exception' => $e,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Une erreur est survenue lors de la recuperation des messages.',
|
|
||||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function stats(): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
if (! $user) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Utilisateur non authentifie.',
|
|
||||||
], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => $this->webmailRepository->statsForUser((int) $user->id),
|
|
||||||
'message' => 'Statistiques webmail recuperees avec succes.',
|
|
||||||
]);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('Error fetching webmail stats: ' . $e->getMessage(), [
|
|
||||||
'exception' => $e,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Une erreur est survenue lors de la recuperation des statistiques webmail.',
|
|
||||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function show(string $id): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
if (! $user) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Utilisateur non authentifie.',
|
|
||||||
], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$message = $this->webmailRepository->findForUser($id, (int) $user->id);
|
|
||||||
|
|
||||||
if (! $message) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Message non trouve.',
|
|
||||||
], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => (new WebmailMessageResource($message))->resolve(),
|
|
||||||
'message' => 'Message recupere avec succes.',
|
|
||||||
]);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('Error fetching webmail message: ' . $e->getMessage(), [
|
|
||||||
'exception' => $e,
|
|
||||||
'message_id' => $id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Une erreur est survenue lors de la recuperation du message.',
|
|
||||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function send(SendWebmailMessageRequest $request): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
if (! $user) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Utilisateur non authentifie.',
|
|
||||||
], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$message = $this->webmailService->send($request->validated(), $user);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => (new WebmailMessageResource($message))->resolve(),
|
|
||||||
'message' => 'Email envoye avec succes.',
|
|
||||||
], 201);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('Error sending webmail message: ' . $e->getMessage(), [
|
|
||||||
'exception' => $e,
|
|
||||||
'data' => $request->validated(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Une erreur est survenue lors de l\'envoi de l\'email.',
|
|
||||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function receive(ReceiveWebmailMessageRequest $request): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
if (! $user) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Utilisateur non authentifie.',
|
|
||||||
], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$message = $this->webmailService->receive($request->validated(), $user);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => (new WebmailMessageResource($message))->resolve(),
|
|
||||||
'message' => 'Email recu et enregistre avec succes.',
|
|
||||||
], 201);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('Error receiving webmail message: ' . $e->getMessage(), [
|
|
||||||
'exception' => $e,
|
|
||||||
'data' => $request->validated(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Une erreur est survenue lors de l\'enregistrement du message recu.',
|
|
||||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function syncMailtrap(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
if (! $user) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Utilisateur non authentifie.',
|
|
||||||
], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $this->webmailService->syncMailtrapInbox(
|
|
||||||
$user,
|
|
||||||
max(1, min(50, (int) $request->integer('limit', 30)))
|
|
||||||
);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => $result,
|
|
||||||
'message' => 'Synchronisation Mailtrap terminee avec succes.',
|
|
||||||
]);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('Error syncing Mailtrap webmail messages: ' . $e->getMessage(), [
|
|
||||||
'exception' => $e,
|
|
||||||
'user_id' => Auth::id(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Une erreur est survenue lors de la synchronisation Mailtrap.',
|
|
||||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function sync(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
if (! $user) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Utilisateur non authentifie.',
|
|
||||||
], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $this->webmailService->syncMailbox(
|
|
||||||
$user->loadMissing('mailboxSetting'),
|
|
||||||
max(1, min(50, (int) $request->integer('limit', 30)))
|
|
||||||
);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => $result,
|
|
||||||
'message' => 'Synchronisation de la boite mail terminee avec succes.',
|
|
||||||
]);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('Error syncing mailbox webmail messages: ' . $e->getMessage(), [
|
|
||||||
'exception' => $e,
|
|
||||||
'user_id' => Auth::id(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Une erreur est survenue lors de la synchronisation de la boite mail.',
|
|
||||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mailboxSettings(): JsonResponse
|
|
||||||
{
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
if (! $user) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Utilisateur non authentifie.',
|
|
||||||
], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => $user->relationLoaded('mailboxSetting')
|
|
||||||
? ($user->mailboxSetting ? (new UserMailboxSettingResource($user->mailboxSetting))->resolve() : null)
|
|
||||||
: ($user->mailboxSetting ? (new UserMailboxSettingResource($user->mailboxSetting))->resolve() : null),
|
|
||||||
'message' => 'Configuration mailbox recuperee avec succes.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function upsertMailboxSettings(UpsertUserMailboxSettingRequest $request): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
if (! $user) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Utilisateur non authentifie.',
|
|
||||||
], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$validated = $request->validated();
|
|
||||||
$clearImapPassword = (bool) ($validated['clear_imap_password'] ?? false);
|
|
||||||
$clearSmtpPassword = (bool) ($validated['clear_smtp_password'] ?? false);
|
|
||||||
|
|
||||||
unset($validated['clear_imap_password'], $validated['clear_smtp_password']);
|
|
||||||
|
|
||||||
if ($clearImapPassword) {
|
|
||||||
$validated['imap_password'] = null;
|
|
||||||
} elseif (array_key_exists('imap_password', $validated) && $validated['imap_password'] === null) {
|
|
||||||
unset($validated['imap_password']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($clearSmtpPassword) {
|
|
||||||
$validated['smtp_password'] = null;
|
|
||||||
} elseif (array_key_exists('smtp_password', $validated) && $validated['smtp_password'] === null) {
|
|
||||||
unset($validated['smtp_password']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var UserMailboxSetting $settings */
|
|
||||||
$settings = UserMailboxSetting::query()->updateOrCreate(
|
|
||||||
['user_id' => $user->id],
|
|
||||||
$validated,
|
|
||||||
);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => (new UserMailboxSettingResource($settings))->resolve(),
|
|
||||||
'message' => 'Configuration mailbox mise a jour avec succes.',
|
|
||||||
]);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('Error updating mailbox settings: ' . $e->getMessage(), [
|
|
||||||
'exception' => $e,
|
|
||||||
'user_id' => Auth::id(),
|
|
||||||
'data' => $request->validated(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Une erreur est survenue lors de la mise a jour de la configuration mailbox.',
|
|
||||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(UpdateWebmailMessageRequest $request, string $id): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
if (! $user) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Utilisateur non authentifie.',
|
|
||||||
], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$message = $this->webmailRepository->findForUser($id, (int) $user->id);
|
|
||||||
|
|
||||||
if (! $message) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Message non trouve.',
|
|
||||||
], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$validated = $request->validated();
|
|
||||||
|
|
||||||
if (array_key_exists('is_read', $validated)) {
|
|
||||||
$validated['read_at'] = $validated['is_read'] ? now() : null;
|
|
||||||
unset($validated['is_read']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (array_key_exists('is_starred', $validated)) {
|
|
||||||
$validated['starred_at'] = $validated['is_starred'] ? now() : null;
|
|
||||||
unset($validated['is_starred']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$updated = $this->webmailRepository->update($id, $validated);
|
|
||||||
|
|
||||||
if (! $updated) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Echec de la mise a jour du message.',
|
|
||||||
], 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var WebmailMessage $freshMessage */
|
|
||||||
$freshMessage = $this->webmailRepository->findForUser($id, (int) $user->id);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => (new WebmailMessageResource($freshMessage))->resolve(),
|
|
||||||
'message' => 'Message mis a jour avec succes.',
|
|
||||||
]);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('Error updating webmail message: ' . $e->getMessage(), [
|
|
||||||
'exception' => $e,
|
|
||||||
'message_id' => $id,
|
|
||||||
'data' => $request->validated(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Une erreur est survenue lors de la mise a jour du message.',
|
|
||||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy(string $id): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
if (! $user) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Utilisateur non authentifie.',
|
|
||||||
], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$message = $this->webmailRepository->findForUser($id, (int) $user->id);
|
|
||||||
|
|
||||||
if (! $message) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Message non trouve.',
|
|
||||||
], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$deleted = $this->webmailRepository->delete($id);
|
|
||||||
|
|
||||||
if (! $deleted) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Echec de la suppression du message.',
|
|
||||||
], 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Message supprime avec succes.',
|
|
||||||
]);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('Error deleting webmail message: ' . $e->getMessage(), [
|
|
||||||
'exception' => $e,
|
|
||||||
'message_id' => $id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Une erreur est survenue lors de la suppression du message.',
|
|
||||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class ReceiveWebmailMessageRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'from_email' => ['required', 'email:rfc,dns'],
|
|
||||||
'from_name' => ['nullable', 'string', 'max:255'],
|
|
||||||
'to' => ['required', 'array', 'min:1'],
|
|
||||||
'to.*' => ['required', 'email:rfc,dns'],
|
|
||||||
'cc' => ['nullable', 'array'],
|
|
||||||
'cc.*' => ['email:rfc,dns'],
|
|
||||||
'bcc' => ['nullable', 'array'],
|
|
||||||
'bcc.*' => ['email:rfc,dns'],
|
|
||||||
'subject' => ['nullable', 'string', 'max:255'],
|
|
||||||
'body' => ['nullable', 'string'],
|
|
||||||
'folder' => ['nullable', 'string', 'max:30'],
|
|
||||||
'status' => ['nullable', 'string', 'max:30'],
|
|
||||||
'received_at' => ['nullable', 'date'],
|
|
||||||
'attachments' => ['nullable', 'array'],
|
|
||||||
'metadata' => ['nullable', 'array'],
|
|
||||||
'message_uid' => ['nullable', 'string', 'max:255'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class SendWebmailMessageRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'to' => ['required', 'array', 'min:1'],
|
|
||||||
'to.*' => ['required', 'email:rfc,dns'],
|
|
||||||
'cc' => ['nullable', 'array'],
|
|
||||||
'cc.*' => ['email:rfc,dns'],
|
|
||||||
'bcc' => ['nullable', 'array'],
|
|
||||||
'bcc.*' => ['email:rfc,dns'],
|
|
||||||
'subject' => ['nullable', 'string', 'max:255'],
|
|
||||||
'body' => ['required', 'string'],
|
|
||||||
'folder' => ['nullable', 'string', 'max:30'],
|
|
||||||
'attachments' => ['nullable', 'array'],
|
|
||||||
'metadata' => ['nullable', 'array'],
|
|
||||||
'message_uid' => ['nullable', 'string', 'max:255'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class UpdateWebmailMessageRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'folder' => ['nullable', 'string', 'max:30'],
|
|
||||||
'status' => ['nullable', 'string', 'max:30'],
|
|
||||||
'is_read' => ['nullable', 'boolean'],
|
|
||||||
'is_starred' => ['nullable', 'boolean'],
|
|
||||||
'subject' => ['nullable', 'string', 'max:255'],
|
|
||||||
'body' => ['nullable', 'string'],
|
|
||||||
'metadata' => ['nullable', 'array'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class UpsertUserMailboxSettingRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'imap_host' => ['nullable', 'string', 'max:255'],
|
|
||||||
'imap_port' => ['nullable', 'integer', 'min:1', 'max:65535'],
|
|
||||||
'imap_encryption' => ['nullable', 'string', 'in:ssl,tls,starttls,none'],
|
|
||||||
'imap_validate_cert' => ['nullable', 'boolean'],
|
|
||||||
'imap_username' => ['nullable', 'string', 'max:255'],
|
|
||||||
'imap_password' => ['nullable', 'string', 'max:500'],
|
|
||||||
'imap_folder' => ['nullable', 'string', 'max:255'],
|
|
||||||
'smtp_host' => ['nullable', 'string', 'max:255'],
|
|
||||||
'smtp_port' => ['nullable', 'integer', 'min:1', 'max:65535'],
|
|
||||||
'smtp_encryption' => ['nullable', 'string', 'in:ssl,tls,none'],
|
|
||||||
'smtp_validate_cert' => ['nullable', 'boolean'],
|
|
||||||
'smtp_username' => ['nullable', 'string', 'max:255'],
|
|
||||||
'smtp_password' => ['nullable', 'string', 'max:500'],
|
|
||||||
'smtp_from_address' => ['nullable', 'email:rfc,dns'],
|
|
||||||
'smtp_from_name' => ['nullable', 'string', 'max:255'],
|
|
||||||
'clear_imap_password' => ['nullable', 'boolean'],
|
|
||||||
'clear_smtp_password' => ['nullable', 'boolean'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Resources\Webmail;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
|
||||||
|
|
||||||
class UserMailboxSettingResource extends JsonResource
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function toArray(Request $request): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'id' => $this->id,
|
|
||||||
'user_id' => $this->user_id,
|
|
||||||
'imap_host' => $this->imap_host,
|
|
||||||
'imap_port' => $this->imap_port,
|
|
||||||
'imap_encryption' => $this->imap_encryption,
|
|
||||||
'imap_validate_cert' => (bool) $this->imap_validate_cert,
|
|
||||||
'imap_username' => $this->imap_username,
|
|
||||||
'imap_folder' => $this->imap_folder,
|
|
||||||
'imap_password_configured' => filled($this->imap_password),
|
|
||||||
'smtp_host' => $this->smtp_host,
|
|
||||||
'smtp_port' => $this->smtp_port,
|
|
||||||
'smtp_encryption' => $this->smtp_encryption,
|
|
||||||
'smtp_validate_cert' => (bool) $this->smtp_validate_cert,
|
|
||||||
'smtp_username' => $this->smtp_username,
|
|
||||||
'smtp_from_address' => $this->smtp_from_address,
|
|
||||||
'smtp_from_name' => $this->smtp_from_name,
|
|
||||||
'smtp_password_configured' => filled($this->smtp_password),
|
|
||||||
'has_imap_configuration' => $this->hasImapConfiguration(),
|
|
||||||
'has_smtp_configuration' => $this->hasSmtpConfiguration(),
|
|
||||||
'last_synced_at' => $this->last_synced_at,
|
|
||||||
'last_sync_error' => $this->last_sync_error,
|
|
||||||
'created_at' => $this->created_at,
|
|
||||||
'updated_at' => $this->updated_at,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Resources\Webmail;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
|
||||||
|
|
||||||
class WebmailMessageResource extends JsonResource
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function toArray(Request $request): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'id' => $this->id,
|
|
||||||
'user_id' => $this->user_id,
|
|
||||||
'message_uid' => $this->message_uid,
|
|
||||||
'direction' => $this->direction,
|
|
||||||
'folder' => $this->folder,
|
|
||||||
'status' => $this->status,
|
|
||||||
'from_email' => $this->from_email,
|
|
||||||
'from_name' => $this->from_name,
|
|
||||||
'to' => $this->to_recipients ?? [],
|
|
||||||
'cc' => $this->cc_recipients ?? [],
|
|
||||||
'bcc' => $this->bcc_recipients ?? [],
|
|
||||||
'subject' => $this->subject,
|
|
||||||
'body' => $this->body,
|
|
||||||
'snippet' => $this->snippet,
|
|
||||||
'attachments' => $this->attachments ?? [],
|
|
||||||
'metadata' => $this->metadata ?? [],
|
|
||||||
'is_read' => $this->read_at !== null,
|
|
||||||
'is_starred' => $this->starred_at !== null,
|
|
||||||
'read_at' => $this->read_at,
|
|
||||||
'starred_at' => $this->starred_at,
|
|
||||||
'sent_at' => $this->sent_at,
|
|
||||||
'received_at' => $this->received_at,
|
|
||||||
'created_at' => $this->created_at,
|
|
||||||
'updated_at' => $this->updated_at,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Mail;
|
|
||||||
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Mail\Mailable;
|
|
||||||
use Illuminate\Mail\Mailables\Content;
|
|
||||||
use Illuminate\Mail\Mailables\Envelope;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class WebmailMessageMail extends Mailable
|
|
||||||
{
|
|
||||||
use Queueable, SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, string> $payload
|
|
||||||
*/
|
|
||||||
public function __construct(public array $payload)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public function envelope(): Envelope
|
|
||||||
{
|
|
||||||
return new Envelope(
|
|
||||||
subject: $this->payload['subject'] ?? 'Nouveau message',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function content(): Content
|
|
||||||
{
|
|
||||||
return new Content(
|
|
||||||
view: 'emails.webmail_message',
|
|
||||||
with: [
|
|
||||||
'body' => $this->payload['body'] ?? '',
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -60,9 +60,4 @@ class User extends Authenticatable
|
|||||||
{
|
{
|
||||||
return $this->hasOne(Employee::class);
|
return $this->hasOne(Employee::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mailboxSetting(): HasOne
|
|
||||||
{
|
|
||||||
return $this->hasOne(UserMailboxSetting::class);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,70 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
|
|
||||||
class UserMailboxSetting extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<int, string>
|
|
||||||
*/
|
|
||||||
protected $fillable = [
|
|
||||||
'user_id',
|
|
||||||
'imap_host',
|
|
||||||
'imap_port',
|
|
||||||
'imap_encryption',
|
|
||||||
'imap_validate_cert',
|
|
||||||
'imap_username',
|
|
||||||
'imap_password',
|
|
||||||
'imap_folder',
|
|
||||||
'smtp_host',
|
|
||||||
'smtp_port',
|
|
||||||
'smtp_encryption',
|
|
||||||
'smtp_validate_cert',
|
|
||||||
'smtp_username',
|
|
||||||
'smtp_password',
|
|
||||||
'smtp_from_address',
|
|
||||||
'smtp_from_name',
|
|
||||||
'last_synced_at',
|
|
||||||
'last_sync_error',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<string, string>
|
|
||||||
*/
|
|
||||||
protected $casts = [
|
|
||||||
'imap_validate_cert' => 'boolean',
|
|
||||||
'smtp_validate_cert' => 'boolean',
|
|
||||||
'imap_password' => 'encrypted',
|
|
||||||
'smtp_password' => 'encrypted',
|
|
||||||
'last_synced_at' => 'datetime',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function user(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function hasImapConfiguration(): bool
|
|
||||||
{
|
|
||||||
return filled($this->imap_host)
|
|
||||||
&& filled($this->imap_port)
|
|
||||||
&& filled($this->imap_username)
|
|
||||||
&& filled($this->imap_password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function hasSmtpConfiguration(): bool
|
|
||||||
{
|
|
||||||
return filled($this->smtp_host)
|
|
||||||
&& filled($this->smtp_port)
|
|
||||||
&& filled($this->smtp_username)
|
|
||||||
&& filled($this->smtp_password);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
|
|
||||||
class WebmailMessage extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<int, string>
|
|
||||||
*/
|
|
||||||
protected $fillable = [
|
|
||||||
'user_id',
|
|
||||||
'message_uid',
|
|
||||||
'direction',
|
|
||||||
'folder',
|
|
||||||
'status',
|
|
||||||
'from_email',
|
|
||||||
'from_name',
|
|
||||||
'to_recipients',
|
|
||||||
'cc_recipients',
|
|
||||||
'bcc_recipients',
|
|
||||||
'subject',
|
|
||||||
'body',
|
|
||||||
'snippet',
|
|
||||||
'attachments',
|
|
||||||
'metadata',
|
|
||||||
'read_at',
|
|
||||||
'starred_at',
|
|
||||||
'sent_at',
|
|
||||||
'received_at',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<string, string>
|
|
||||||
*/
|
|
||||||
protected $casts = [
|
|
||||||
'to_recipients' => 'array',
|
|
||||||
'cc_recipients' => 'array',
|
|
||||||
'bcc_recipients' => 'array',
|
|
||||||
'attachments' => 'array',
|
|
||||||
'metadata' => 'array',
|
|
||||||
'read_at' => 'datetime',
|
|
||||||
'starred_at' => 'datetime',
|
|
||||||
'sent_at' => 'datetime',
|
|
||||||
'received_at' => 'datetime',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function user(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -12,8 +12,6 @@ use App\Repositories\InterventionRepositoryInterface;
|
|||||||
use App\Repositories\InterventionRepository;
|
use App\Repositories\InterventionRepository;
|
||||||
use App\Repositories\FileRepositoryInterface;
|
use App\Repositories\FileRepositoryInterface;
|
||||||
use App\Repositories\FileRepository;
|
use App\Repositories\FileRepository;
|
||||||
use App\Repositories\WebmailMessageRepository;
|
|
||||||
use App\Repositories\WebmailMessageRepositoryInterface;
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class RepositoryServiceProvider extends ServiceProvider
|
class RepositoryServiceProvider extends ServiceProvider
|
||||||
@ -28,7 +26,6 @@ class RepositoryServiceProvider extends ServiceProvider
|
|||||||
$this->app->bind(DeceasedDocumentRepositoryInterface::class, DeceasedDocumentRepository::class);
|
$this->app->bind(DeceasedDocumentRepositoryInterface::class, DeceasedDocumentRepository::class);
|
||||||
$this->app->bind(InterventionRepositoryInterface::class, InterventionRepository::class);
|
$this->app->bind(InterventionRepositoryInterface::class, InterventionRepository::class);
|
||||||
$this->app->bind(FileRepositoryInterface::class, FileRepository::class);
|
$this->app->bind(FileRepositoryInterface::class, FileRepository::class);
|
||||||
$this->app->bind(WebmailMessageRepositoryInterface::class, WebmailMessageRepository::class);
|
|
||||||
$this->app->bind(\App\Repositories\PurchaseOrderRepositoryInterface::class, \App\Repositories\PurchaseOrderRepository::class);
|
$this->app->bind(\App\Repositories\PurchaseOrderRepositoryInterface::class, \App\Repositories\PurchaseOrderRepository::class);
|
||||||
$this->app->bind(\App\Repositories\WarehouseRepositoryInterface::class, \App\Repositories\WarehouseRepository::class);
|
$this->app->bind(\App\Repositories\WarehouseRepositoryInterface::class, \App\Repositories\WarehouseRepository::class);
|
||||||
$this->app->bind(\App\Repositories\StockItemRepositoryInterface::class, \App\Repositories\StockItemRepository::class);
|
$this->app->bind(\App\Repositories\StockItemRepositoryInterface::class, \App\Repositories\StockItemRepository::class);
|
||||||
|
|||||||
@ -1,82 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Repositories;
|
|
||||||
|
|
||||||
use App\Models\WebmailMessage;
|
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
||||||
|
|
||||||
class WebmailMessageRepository extends BaseRepository implements WebmailMessageRepositoryInterface
|
|
||||||
{
|
|
||||||
public function __construct(WebmailMessage $model)
|
|
||||||
{
|
|
||||||
parent::__construct($model);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function paginateForUser(int $userId, array $filters = [], int $perPage = 15): LengthAwarePaginator
|
|
||||||
{
|
|
||||||
$query = $this->model->newQuery()
|
|
||||||
->where('user_id', $userId)
|
|
||||||
->orderByRaw('COALESCE(received_at, sent_at, created_at) DESC');
|
|
||||||
|
|
||||||
if (!empty($filters['folder'])) {
|
|
||||||
$query->where('folder', $filters['folder']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($filters['status'])) {
|
|
||||||
$query->where('status', $filters['status']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (array_key_exists('unread', $filters) && $filters['unread'] !== null) {
|
|
||||||
$filters['unread']
|
|
||||||
? $query->whereNull('read_at')
|
|
||||||
: $query->whereNotNull('read_at');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (array_key_exists('starred', $filters) && $filters['starred'] !== null) {
|
|
||||||
$filters['starred']
|
|
||||||
? $query->whereNotNull('starred_at')
|
|
||||||
: $query->whereNull('starred_at');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($filters['search'])) {
|
|
||||||
$search = '%' . trim((string) $filters['search']) . '%';
|
|
||||||
|
|
||||||
$query->where(function ($builder) use ($search): void {
|
|
||||||
$builder
|
|
||||||
->where('subject', 'like', $search)
|
|
||||||
->orWhere('from_email', 'like', $search)
|
|
||||||
->orWhere('from_name', 'like', $search)
|
|
||||||
->orWhere('body', 'like', $search)
|
|
||||||
->orWhere('snippet', 'like', $search);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return $query->paginate($perPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function findForUser(int|string $id, int $userId): ?WebmailMessage
|
|
||||||
{
|
|
||||||
$message = $this->model->newQuery()
|
|
||||||
->where('user_id', $userId)
|
|
||||||
->find($id);
|
|
||||||
|
|
||||||
return $message instanceof WebmailMessage ? $message : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function statsForUser(int $userId): array
|
|
||||||
{
|
|
||||||
$baseQuery = $this->model->newQuery()->where('user_id', $userId);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'total' => (clone $baseQuery)->count(),
|
|
||||||
'inbox' => (clone $baseQuery)->where('folder', 'inbox')->count(),
|
|
||||||
'sent' => (clone $baseQuery)->where('folder', 'sent')->count(),
|
|
||||||
'drafts' => (clone $baseQuery)->where('folder', 'drafts')->count(),
|
|
||||||
'trash' => (clone $baseQuery)->where('folder', 'trash')->count(),
|
|
||||||
'unread' => (clone $baseQuery)->whereNull('read_at')->count(),
|
|
||||||
'starred' => (clone $baseQuery)->whereNotNull('starred_at')->count(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Repositories;
|
|
||||||
|
|
||||||
use App\Models\WebmailMessage;
|
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
||||||
|
|
||||||
interface WebmailMessageRepositoryInterface extends BaseRepositoryInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $filters
|
|
||||||
*/
|
|
||||||
public function paginateForUser(int $userId, array $filters = [], int $perPage = 15): LengthAwarePaginator;
|
|
||||||
|
|
||||||
public function findForUser(int|string $id, int $userId): ?WebmailMessage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, int>
|
|
||||||
*/
|
|
||||||
public function statsForUser(int $userId): array;
|
|
||||||
}
|
|
||||||
@ -1,524 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
use App\Mail\WebmailMessageMail;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\UserMailboxSetting;
|
|
||||||
use App\Models\WebmailMessage;
|
|
||||||
use App\Repositories\WebmailMessageRepositoryInterface;
|
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
use Illuminate\Support\Carbon;
|
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use Illuminate\Support\Facades\Mail;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Symfony\Component\Mailer\Mailer;
|
|
||||||
use Symfony\Component\Mailer\Transport;
|
|
||||||
use Symfony\Component\Mime\Address;
|
|
||||||
use Symfony\Component\Mime\Email;
|
|
||||||
use Throwable;
|
|
||||||
use Webklex\PHPIMAP\Attribute;
|
|
||||||
use Webklex\PHPIMAP\ClientManager;
|
|
||||||
use Webklex\PHPIMAP\Message as ImapMessage;
|
|
||||||
|
|
||||||
class WebmailService
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly WebmailMessageRepositoryInterface $webmailRepository
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $payload
|
|
||||||
*/
|
|
||||||
public function send(array $payload, User $user): WebmailMessage
|
|
||||||
{
|
|
||||||
$fromEmail = $user->email ?: config('mail.from.address');
|
|
||||||
$fromName = $user->name ?: config('mail.from.name');
|
|
||||||
$mailboxSetting = $user->mailboxSetting;
|
|
||||||
|
|
||||||
$message = $this->webmailRepository->create([
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'message_uid' => $payload['message_uid'] ?? (string) Str::uuid(),
|
|
||||||
'direction' => 'outgoing',
|
|
||||||
'folder' => $payload['folder'] ?? 'sent',
|
|
||||||
'status' => 'pending',
|
|
||||||
'from_email' => $fromEmail,
|
|
||||||
'from_name' => $fromName,
|
|
||||||
'to_recipients' => $payload['to'],
|
|
||||||
'cc_recipients' => $payload['cc'] ?? [],
|
|
||||||
'bcc_recipients' => $payload['bcc'] ?? [],
|
|
||||||
'subject' => $payload['subject'] ?? null,
|
|
||||||
'body' => $payload['body'] ?? '',
|
|
||||||
'snippet' => $this->makeSnippet($payload['body'] ?? ''),
|
|
||||||
'attachments' => $payload['attachments'] ?? [],
|
|
||||||
'metadata' => $payload['metadata'] ?? [],
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ($mailboxSetting instanceof UserMailboxSetting && $mailboxSetting->hasSmtpConfiguration()) {
|
|
||||||
$this->sendUsingUserSmtp($payload, $mailboxSetting, $fromEmail, $fromName);
|
|
||||||
} else {
|
|
||||||
$mailable = new WebmailMessageMail([
|
|
||||||
'subject' => $payload['subject'] ?? '',
|
|
||||||
'body' => $payload['body'] ?? '',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (! empty($fromEmail)) {
|
|
||||||
$mailable->from($fromEmail, $fromName);
|
|
||||||
$mailable->replyTo($fromEmail, $fromName);
|
|
||||||
}
|
|
||||||
|
|
||||||
$mailer = Mail::to($payload['to']);
|
|
||||||
|
|
||||||
if (! empty($payload['cc'])) {
|
|
||||||
$mailer->cc($payload['cc']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($payload['bcc'])) {
|
|
||||||
$mailer->bcc($payload['bcc']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$mailer->send($mailable);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->webmailRepository->update($message->id, [
|
|
||||||
'status' => 'sent',
|
|
||||||
'sent_at' => now(),
|
|
||||||
'metadata' => array_merge(Arr::wrap($message->metadata), [
|
|
||||||
'transport' => $mailboxSetting instanceof UserMailboxSetting && $mailboxSetting->hasSmtpConfiguration()
|
|
||||||
? 'user_smtp'
|
|
||||||
: 'app_mailer',
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
} catch (Throwable $throwable) {
|
|
||||||
$metadata = Arr::wrap($message->metadata);
|
|
||||||
$metadata['send_error'] = $throwable->getMessage();
|
|
||||||
|
|
||||||
$this->webmailRepository->update($message->id, [
|
|
||||||
'status' => 'failed',
|
|
||||||
'metadata' => $metadata,
|
|
||||||
]);
|
|
||||||
|
|
||||||
throw $throwable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var WebmailMessage $freshMessage */
|
|
||||||
$freshMessage = $this->webmailRepository->find((string) $message->id);
|
|
||||||
|
|
||||||
return $freshMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{imported:int, skipped:int, source:string}
|
|
||||||
*/
|
|
||||||
public function syncMailbox(User $user, int $limit = 30): array
|
|
||||||
{
|
|
||||||
$mailboxSetting = $user->mailboxSetting;
|
|
||||||
|
|
||||||
if ($mailboxSetting instanceof UserMailboxSetting && $mailboxSetting->hasImapConfiguration()) {
|
|
||||||
return $this->syncImapInbox($user, $mailboxSetting, $limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $this->syncMailtrapInbox($user, $limit);
|
|
||||||
$result['source'] = 'mailtrap';
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{imported:int, skipped:int}
|
|
||||||
*/
|
|
||||||
public function syncMailtrapInbox(User $user, int $limit = 30): array
|
|
||||||
{
|
|
||||||
$accountId = (string) config('services.mailtrap.account_id');
|
|
||||||
$inboxId = (string) config('services.mailtrap.inbox_id');
|
|
||||||
$token = (string) config('services.mailtrap.token');
|
|
||||||
$baseUrl = rtrim((string) config('services.mailtrap.base_url', 'https://mailtrap.io'), '/');
|
|
||||||
|
|
||||||
if ($accountId === '' || $inboxId === '' || $token === '') {
|
|
||||||
throw new \RuntimeException('La configuration Mailtrap est incomplete. Renseignez MAILTRAP_ACCOUNT_ID, MAILTRAP_INBOX_ID et MAILTRAP_API_TOKEN.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($user->email)) {
|
|
||||||
throw new \RuntimeException('L\'utilisateur connecte ne possede pas d\'adresse email.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = Http::baseUrl($baseUrl)
|
|
||||||
->acceptJson()
|
|
||||||
->withToken($token)
|
|
||||||
->get("/api/accounts/{$accountId}/inboxes/{$inboxId}/messages");
|
|
||||||
|
|
||||||
$response->throw();
|
|
||||||
|
|
||||||
/** @var array<int, array<string, mixed>> $messages */
|
|
||||||
$messages = $response->json();
|
|
||||||
$imported = 0;
|
|
||||||
$skipped = 0;
|
|
||||||
|
|
||||||
foreach (array_slice($messages, 0, max(1, $limit)) as $sandboxMessage) {
|
|
||||||
if (! $this->messageTargetsUser($sandboxMessage, (string) $user->email)) {
|
|
||||||
$skipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$messageUid = 'mailtrap-' . (string) ($sandboxMessage['id'] ?? Str::uuid());
|
|
||||||
|
|
||||||
$alreadyExists = WebmailMessage::query()
|
|
||||||
->where('user_id', $user->id)
|
|
||||||
->where('message_uid', $messageUid)
|
|
||||||
->exists();
|
|
||||||
|
|
||||||
if ($alreadyExists) {
|
|
||||||
$skipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$messageId = (string) ($sandboxMessage['id'] ?? '');
|
|
||||||
$body = $messageId !== ''
|
|
||||||
? $this->fetchMailtrapBody($baseUrl, $token, $accountId, $inboxId, $messageId)
|
|
||||||
: '';
|
|
||||||
|
|
||||||
$this->receive([
|
|
||||||
'message_uid' => $messageUid,
|
|
||||||
'from_email' => $sandboxMessage['from_email'] ?? 'unknown@mailtrap.local',
|
|
||||||
'from_name' => $sandboxMessage['from_name'] ?? null,
|
|
||||||
'to' => $this->extractRecipients($sandboxMessage),
|
|
||||||
'subject' => $sandboxMessage['subject'] ?? null,
|
|
||||||
'body' => $body,
|
|
||||||
'status' => 'received',
|
|
||||||
'folder' => 'inbox',
|
|
||||||
'received_at' => $sandboxMessage['sent_at'] ?? now()->toDateTimeString(),
|
|
||||||
'metadata' => [
|
|
||||||
'provider' => 'mailtrap',
|
|
||||||
'mailtrap_inbox_id' => $inboxId,
|
|
||||||
'mailtrap_message_id' => $sandboxMessage['id'] ?? null,
|
|
||||||
'mailtrap_raw' => $sandboxMessage,
|
|
||||||
],
|
|
||||||
], $user);
|
|
||||||
|
|
||||||
$imported++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'imported' => $imported,
|
|
||||||
'skipped' => $skipped,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $payload
|
|
||||||
*/
|
|
||||||
public function receive(array $payload, User $user): WebmailMessage
|
|
||||||
{
|
|
||||||
$receivedAt = !empty($payload['received_at'])
|
|
||||||
? Carbon::parse((string) $payload['received_at'])
|
|
||||||
: now();
|
|
||||||
|
|
||||||
/** @var WebmailMessage $message */
|
|
||||||
$message = $this->webmailRepository->create([
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'message_uid' => $payload['message_uid'] ?? (string) Str::uuid(),
|
|
||||||
'direction' => 'incoming',
|
|
||||||
'folder' => $payload['folder'] ?? 'inbox',
|
|
||||||
'status' => $payload['status'] ?? 'received',
|
|
||||||
'from_email' => $payload['from_email'],
|
|
||||||
'from_name' => $payload['from_name'] ?? null,
|
|
||||||
'to_recipients' => $payload['to'],
|
|
||||||
'cc_recipients' => $payload['cc'] ?? [],
|
|
||||||
'bcc_recipients' => $payload['bcc'] ?? [],
|
|
||||||
'subject' => $payload['subject'] ?? null,
|
|
||||||
'body' => $payload['body'] ?? '',
|
|
||||||
'snippet' => $this->makeSnippet($payload['body'] ?? ''),
|
|
||||||
'attachments' => $payload['attachments'] ?? [],
|
|
||||||
'metadata' => $payload['metadata'] ?? [],
|
|
||||||
'received_at' => $receivedAt,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $message;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function makeSnippet(string $body): string
|
|
||||||
{
|
|
||||||
return Str::limit(trim(strip_tags($body)), 160, '...');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function sendUsingUserSmtp(
|
|
||||||
array $payload,
|
|
||||||
UserMailboxSetting $mailboxSetting,
|
|
||||||
string $fallbackFromEmail,
|
|
||||||
?string $fallbackFromName,
|
|
||||||
): void {
|
|
||||||
$transport = Transport::fromDsn($this->buildSmtpDsn($mailboxSetting));
|
|
||||||
$mailer = new Mailer($transport);
|
|
||||||
|
|
||||||
$fromEmail = $mailboxSetting->smtp_from_address ?: $fallbackFromEmail;
|
|
||||||
$fromName = $mailboxSetting->smtp_from_name ?: $fallbackFromName;
|
|
||||||
$htmlBody = view('emails.webmail_message', [
|
|
||||||
'body' => $payload['body'] ?? '',
|
|
||||||
])->render();
|
|
||||||
|
|
||||||
$email = (new Email())
|
|
||||||
->from(new Address($fromEmail, $fromName ?? ''))
|
|
||||||
->replyTo(new Address($fromEmail, $fromName ?? ''))
|
|
||||||
->subject((string) ($payload['subject'] ?? ''))
|
|
||||||
->html($htmlBody)
|
|
||||||
->text(trim(strip_tags((string) ($payload['body'] ?? ''))));
|
|
||||||
|
|
||||||
foreach ($payload['to'] as $recipient) {
|
|
||||||
$email->addTo(new Address((string) $recipient));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($payload['cc'] ?? [] as $recipient) {
|
|
||||||
$email->addCc(new Address((string) $recipient));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($payload['bcc'] ?? [] as $recipient) {
|
|
||||||
$email->addBcc(new Address((string) $recipient));
|
|
||||||
}
|
|
||||||
|
|
||||||
$mailer->send($email);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{imported:int, skipped:int, source:string}
|
|
||||||
*/
|
|
||||||
private function syncImapInbox(User $user, UserMailboxSetting $mailboxSetting, int $limit): array
|
|
||||||
{
|
|
||||||
$imported = 0;
|
|
||||||
$skipped = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
$clientManager = new ClientManager([
|
|
||||||
'options' => [
|
|
||||||
'fetch' => null,
|
|
||||||
],
|
|
||||||
'accounts' => [
|
|
||||||
'default' => [
|
|
||||||
'host' => $mailboxSetting->imap_host,
|
|
||||||
'port' => $mailboxSetting->imap_port,
|
|
||||||
'encryption' => $this->normalizeImapEncryption($mailboxSetting->imap_encryption),
|
|
||||||
'validate_cert' => $mailboxSetting->imap_validate_cert,
|
|
||||||
'username' => $mailboxSetting->imap_username,
|
|
||||||
'password' => $mailboxSetting->imap_password,
|
|
||||||
'protocol' => 'imap',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$client = $clientManager->account('default');
|
|
||||||
$client->connect();
|
|
||||||
|
|
||||||
$folder = $client->getFolder($mailboxSetting->imap_folder ?: 'INBOX');
|
|
||||||
$query = $folder->query()
|
|
||||||
->leaveUnread()
|
|
||||||
->setFetchOrderDesc()
|
|
||||||
->limit(max(1, $limit));
|
|
||||||
|
|
||||||
if ($mailboxSetting->last_synced_at) {
|
|
||||||
$query->whereSince($mailboxSetting->last_synced_at->copy()->subDay());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var iterable<int, ImapMessage> $messages */
|
|
||||||
$messages = $query->get();
|
|
||||||
|
|
||||||
foreach ($messages as $imapMessage) {
|
|
||||||
$messageUid = 'imap-' . (string) $imapMessage->getUid();
|
|
||||||
|
|
||||||
$alreadyExists = WebmailMessage::query()
|
|
||||||
->where('user_id', $user->id)
|
|
||||||
->where('message_uid', $messageUid)
|
|
||||||
->exists();
|
|
||||||
|
|
||||||
if ($alreadyExists) {
|
|
||||||
$skipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$body = trim($imapMessage->getTextBody());
|
|
||||||
if ($body === '') {
|
|
||||||
$body = trim(strip_tags($imapMessage->getHTMLBody()));
|
|
||||||
}
|
|
||||||
|
|
||||||
$from = $this->extractAddressList($imapMessage->getFrom());
|
|
||||||
$to = $this->extractAddressList($imapMessage->getTo());
|
|
||||||
$cc = $this->extractAddressList($imapMessage->getCc());
|
|
||||||
$bcc = $this->extractAddressList($imapMessage->getBcc());
|
|
||||||
|
|
||||||
$this->receive([
|
|
||||||
'message_uid' => $messageUid,
|
|
||||||
'from_email' => $from[0]['email'] ?? 'unknown@imap.local',
|
|
||||||
'from_name' => $from[0]['name'] ?? null,
|
|
||||||
'to' => array_values(array_filter(array_map(fn (array $item): ?string => $item['email'] ?? null, $to))),
|
|
||||||
'cc' => array_values(array_filter(array_map(fn (array $item): ?string => $item['email'] ?? null, $cc))),
|
|
||||||
'bcc' => array_values(array_filter(array_map(fn (array $item): ?string => $item['email'] ?? null, $bcc))),
|
|
||||||
'subject' => (string) $imapMessage->getSubject(),
|
|
||||||
'body' => $body,
|
|
||||||
'status' => 'received',
|
|
||||||
'folder' => 'inbox',
|
|
||||||
'received_at' => $imapMessage->getDate()->toDate()->toDateTimeString(),
|
|
||||||
'attachments' => [],
|
|
||||||
'metadata' => [
|
|
||||||
'provider' => 'imap',
|
|
||||||
'imap_uid' => (string) $imapMessage->getUid(),
|
|
||||||
],
|
|
||||||
], $user);
|
|
||||||
|
|
||||||
$imported++;
|
|
||||||
}
|
|
||||||
|
|
||||||
$mailboxSetting->forceFill([
|
|
||||||
'last_synced_at' => now(),
|
|
||||||
'last_sync_error' => null,
|
|
||||||
])->save();
|
|
||||||
|
|
||||||
$client->disconnect();
|
|
||||||
} catch (Throwable $throwable) {
|
|
||||||
$mailboxSetting->forceFill([
|
|
||||||
'last_sync_error' => $throwable->getMessage(),
|
|
||||||
])->save();
|
|
||||||
|
|
||||||
throw $throwable;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'imported' => $imported,
|
|
||||||
'skipped' => $skipped,
|
|
||||||
'source' => 'imap',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildSmtpDsn(UserMailboxSetting $mailboxSetting): string
|
|
||||||
{
|
|
||||||
$scheme = ($mailboxSetting->smtp_encryption ?? '') === 'ssl' ? 'smtps' : 'smtp';
|
|
||||||
$username = rawurlencode((string) $mailboxSetting->smtp_username);
|
|
||||||
$password = rawurlencode((string) $mailboxSetting->smtp_password);
|
|
||||||
$host = (string) $mailboxSetting->smtp_host;
|
|
||||||
$port = (int) $mailboxSetting->smtp_port;
|
|
||||||
$query = [];
|
|
||||||
|
|
||||||
if (($mailboxSetting->smtp_encryption ?? '') === 'tls') {
|
|
||||||
$query['encryption'] = 'tls';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $mailboxSetting->smtp_validate_cert) {
|
|
||||||
$query['verify_peer'] = '0';
|
|
||||||
}
|
|
||||||
|
|
||||||
$suffix = $query === [] ? '' : '?' . http_build_query($query);
|
|
||||||
|
|
||||||
return sprintf('%s://%s:%s@%s:%d%s', $scheme, $username, $password, $host, $port, $suffix);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function normalizeImapEncryption(?string $encryption): ?string
|
|
||||||
{
|
|
||||||
if ($encryption === null || $encryption === '' || $encryption === 'none') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $encryption;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<int, array{name:?string,email:?string}>
|
|
||||||
*/
|
|
||||||
private function extractAddressList(mixed $attribute): array
|
|
||||||
{
|
|
||||||
if (! $attribute instanceof Attribute) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return collect($attribute->toArray())
|
|
||||||
->map(function (mixed $item): array {
|
|
||||||
if (is_object($item)) {
|
|
||||||
return [
|
|
||||||
'name' => isset($item->personal) ? (string) $item->personal : null,
|
|
||||||
'email' => isset($item->mail) ? (string) $item->mail : null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_array($item)) {
|
|
||||||
return [
|
|
||||||
'name' => isset($item['personal']) ? (string) $item['personal'] : null,
|
|
||||||
'email' => isset($item['mail']) ? (string) $item['mail'] : null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'name' => null,
|
|
||||||
'email' => is_string($item) ? $item : null,
|
|
||||||
];
|
|
||||||
})
|
|
||||||
->filter(fn (array $item): bool => filled($item['email']))
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $sandboxMessage
|
|
||||||
*/
|
|
||||||
private function messageTargetsUser(array $sandboxMessage, string $userEmail): bool
|
|
||||||
{
|
|
||||||
$userEmail = Str::lower(trim($userEmail));
|
|
||||||
|
|
||||||
return collect($this->extractRecipients($sandboxMessage))
|
|
||||||
->map(fn (mixed $email): string => Str::lower(trim((string) $email)))
|
|
||||||
->contains($userEmail);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $sandboxMessage
|
|
||||||
* @return array<int, string>
|
|
||||||
*/
|
|
||||||
private function extractRecipients(array $sandboxMessage): array
|
|
||||||
{
|
|
||||||
$candidates = [
|
|
||||||
$sandboxMessage['to'] ?? null,
|
|
||||||
$sandboxMessage['to_email'] ?? null,
|
|
||||||
$sandboxMessage['to_emails'] ?? null,
|
|
||||||
];
|
|
||||||
|
|
||||||
return collect($candidates)
|
|
||||||
->flatten(1)
|
|
||||||
->map(function (mixed $recipient): ?string {
|
|
||||||
if (is_array($recipient)) {
|
|
||||||
$email = $recipient['email'] ?? $recipient['address'] ?? $recipient['to_email'] ?? null;
|
|
||||||
return is_string($email) ? trim($email) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return is_string($recipient) ? trim($recipient) : null;
|
|
||||||
})
|
|
||||||
->filter(fn (?string $email): bool => ! empty($email))
|
|
||||||
->unique()
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function fetchMailtrapBody(string $baseUrl, string $token, string $accountId, string $inboxId, string $messageId): string
|
|
||||||
{
|
|
||||||
$textResponse = Http::baseUrl($baseUrl)
|
|
||||||
->withToken($token)
|
|
||||||
->accept('text/plain')
|
|
||||||
->get("/api/accounts/{$accountId}/inboxes/{$inboxId}/messages/{$messageId}/body.txt");
|
|
||||||
|
|
||||||
if ($textResponse->successful()) {
|
|
||||||
$body = trim($textResponse->body());
|
|
||||||
if ($body !== '') {
|
|
||||||
return $body;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$htmlResponse = Http::baseUrl($baseUrl)
|
|
||||||
->withToken($token)
|
|
||||||
->accept('text/html')
|
|
||||||
->get("/api/accounts/{$accountId}/inboxes/{$inboxId}/messages/{$messageId}/body.html");
|
|
||||||
|
|
||||||
if (! $htmlResponse->successful()) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return trim(strip_tags($htmlResponse->body()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -13,9 +13,8 @@
|
|||||||
"barryvdh/laravel-dompdf": "^3.1",
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/sanctum": "^4.2",
|
"laravel/sanctum": "^4.2",
|
||||||
"laravel/tinker": "^2.10.1",
|
|
||||||
"spatie/laravel-permission": "^6.18",
|
"spatie/laravel-permission": "^6.18",
|
||||||
"webklex/php-imap": "^6.2"
|
"laravel/tinker": "^2.10.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
|||||||
83
thanasoft-back/composer.lock
generated
83
thanasoft-back/composer.lock
generated
@ -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": "74fc1ffaa567d424ef63bdd4f9dea808",
|
"content-hash": "39694481426b03a733a81beaf6531e56",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "barryvdh/laravel-dompdf",
|
"name": "barryvdh/laravel-dompdf",
|
||||||
@ -6699,87 +6699,6 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-11-21T01:49:47+00:00"
|
"time": "2024-11-21T01:49:47+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "webklex/php-imap",
|
|
||||||
"version": "6.2.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/Webklex/php-imap.git",
|
|
||||||
"reference": "6b8ef85d621bbbaf52741b00cca8e9237e2b2e05"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/Webklex/php-imap/zipball/6b8ef85d621bbbaf52741b00cca8e9237e2b2e05",
|
|
||||||
"reference": "6b8ef85d621bbbaf52741b00cca8e9237e2b2e05",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-fileinfo": "*",
|
|
||||||
"ext-iconv": "*",
|
|
||||||
"ext-json": "*",
|
|
||||||
"ext-libxml": "*",
|
|
||||||
"ext-mbstring": "*",
|
|
||||||
"ext-openssl": "*",
|
|
||||||
"ext-zip": "*",
|
|
||||||
"illuminate/pagination": ">=5.0.0",
|
|
||||||
"nesbot/carbon": "^2.62.1|^3.2.4",
|
|
||||||
"php": "^8.0.2",
|
|
||||||
"symfony/http-foundation": ">=2.8.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "^9.5.10"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"symfony/mime": "Recomended for better extension support",
|
|
||||||
"symfony/var-dumper": "Usefull tool for debugging"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-master": "6.0-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Webklex\\PHPIMAP\\": "src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Malte Goldenbaum",
|
|
||||||
"email": "github@webklex.com",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "PHP IMAP client",
|
|
||||||
"homepage": "https://github.com/webklex/php-imap",
|
|
||||||
"keywords": [
|
|
||||||
"imap",
|
|
||||||
"mail",
|
|
||||||
"php-imap",
|
|
||||||
"pop3",
|
|
||||||
"webklex"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/Webklex/php-imap/issues",
|
|
||||||
"source": "https://github.com/Webklex/php-imap/tree/6.2.0"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://www.buymeacoffee.com/webklex",
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://ko-fi.com/webklex",
|
|
||||||
"type": "ko_fi"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2025-04-25T06:02:37+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "webmozart/assert",
|
"name": "webmozart/assert",
|
||||||
"version": "1.11.0",
|
"version": "1.11.0",
|
||||||
|
|||||||
@ -35,11 +35,4 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'mailtrap' => [
|
|
||||||
'base_url' => env('MAILTRAP_BASE_URL', 'https://mailtrap.io'),
|
|
||||||
'account_id' => env('MAILTRAP_ACCOUNT_ID'),
|
|
||||||
'inbox_id' => env('MAILTRAP_INBOX_ID'),
|
|
||||||
'token' => env('MAILTRAP_API_TOKEN'),
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,40 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('webmail_messages', function (Blueprint $table): void {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->string('message_uid')->nullable()->index();
|
|
||||||
$table->string('direction', 20);
|
|
||||||
$table->string('folder', 30)->default('inbox')->index();
|
|
||||||
$table->string('status', 30)->default('received')->index();
|
|
||||||
$table->string('from_email')->nullable();
|
|
||||||
$table->string('from_name')->nullable();
|
|
||||||
$table->json('to_recipients');
|
|
||||||
$table->json('cc_recipients')->nullable();
|
|
||||||
$table->json('bcc_recipients')->nullable();
|
|
||||||
$table->string('subject')->nullable();
|
|
||||||
$table->longText('body')->nullable();
|
|
||||||
$table->text('snippet')->nullable();
|
|
||||||
$table->json('attachments')->nullable();
|
|
||||||
$table->json('metadata')->nullable();
|
|
||||||
$table->timestamp('read_at')->nullable()->index();
|
|
||||||
$table->timestamp('starred_at')->nullable()->index();
|
|
||||||
$table->timestamp('sent_at')->nullable()->index();
|
|
||||||
$table->timestamp('received_at')->nullable()->index();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('webmail_messages');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('user_mailbox_settings', function (Blueprint $table): void {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
|
|
||||||
$table->string('imap_host')->nullable();
|
|
||||||
$table->unsignedSmallInteger('imap_port')->nullable();
|
|
||||||
$table->string('imap_encryption', 10)->nullable();
|
|
||||||
$table->boolean('imap_validate_cert')->default(true);
|
|
||||||
$table->string('imap_username')->nullable();
|
|
||||||
$table->text('imap_password')->nullable();
|
|
||||||
$table->string('imap_folder')->default('INBOX');
|
|
||||||
$table->string('smtp_host')->nullable();
|
|
||||||
$table->unsignedSmallInteger('smtp_port')->nullable();
|
|
||||||
$table->string('smtp_encryption', 10)->nullable();
|
|
||||||
$table->boolean('smtp_validate_cert')->default(true);
|
|
||||||
$table->string('smtp_username')->nullable();
|
|
||||||
$table->text('smtp_password')->nullable();
|
|
||||||
$table->string('smtp_from_address')->nullable();
|
|
||||||
$table->string('smtp_from_name')->nullable();
|
|
||||||
$table->timestamp('last_synced_at')->nullable();
|
|
||||||
$table->text('last_sync_error')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('user_mailbox_settings');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Email</title>
|
|
||||||
</head>
|
|
||||||
<body style="margin:0;padding:24px;background:#f8f9fa;font-family:Arial,sans-serif;color:#344767;">
|
|
||||||
<div style="max-width:680px;margin:0 auto;background:#ffffff;border-radius:12px;padding:24px;border:1px solid #e9ecef;">
|
|
||||||
{!! $body !!}
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -28,7 +28,6 @@ use App\Http\Controllers\Api\GoodsReceiptController;
|
|||||||
use App\Http\Controllers\Api\UserController;
|
use App\Http\Controllers\Api\UserController;
|
||||||
use App\Http\Controllers\Api\VehicleController;
|
use App\Http\Controllers\Api\VehicleController;
|
||||||
use App\Http\Controllers\Api\ConvoyController;
|
use App\Http\Controllers\Api\ConvoyController;
|
||||||
use App\Http\Controllers\Api\WebmailController;
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -67,19 +66,6 @@ Route::middleware('auth:sanctum')->group(function () {
|
|||||||
Route::apiResource('client-groups', ClientGroupController::class);
|
Route::apiResource('client-groups', ClientGroupController::class);
|
||||||
Route::apiResource('price-lists', PriceListController::class);
|
Route::apiResource('price-lists', PriceListController::class);
|
||||||
Route::apiResource('users', UserController::class);
|
Route::apiResource('users', UserController::class);
|
||||||
Route::prefix('webmail')->group(function () {
|
|
||||||
Route::get('settings', [WebmailController::class, 'mailboxSettings']);
|
|
||||||
Route::put('settings', [WebmailController::class, 'upsertMailboxSettings']);
|
|
||||||
Route::get('messages/stats', [WebmailController::class, 'stats']);
|
|
||||||
Route::get('messages', [WebmailController::class, 'index']);
|
|
||||||
Route::post('messages/send', [WebmailController::class, 'send']);
|
|
||||||
Route::post('messages/receive', [WebmailController::class, 'receive']);
|
|
||||||
Route::post('messages/sync', [WebmailController::class, 'sync']);
|
|
||||||
Route::post('messages/sync-mailtrap', [WebmailController::class, 'syncMailtrap']);
|
|
||||||
Route::get('messages/{id}', [WebmailController::class, 'show']);
|
|
||||||
Route::patch('messages/{id}', [WebmailController::class, 'update']);
|
|
||||||
Route::delete('messages/{id}', [WebmailController::class, 'destroy']);
|
|
||||||
});
|
|
||||||
Route::middleware('permission:config.view_roles')->group(function () {
|
Route::middleware('permission:config.view_roles')->group(function () {
|
||||||
Route::get('access-control', [AccessControlController::class, 'index']);
|
Route::get('access-control', [AccessControlController::class, 'index']);
|
||||||
Route::post('access-control/roles', [AccessControlController::class, 'storeRole']);
|
Route::post('access-control/roles', [AccessControlController::class, 'storeRole']);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,97 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<client-detail-template>
|
<add-intervention-template>
|
||||||
<template #button-return>
|
<template #intervention-form>
|
||||||
<div class="col-12">
|
|
||||||
<router-link
|
|
||||||
to="/interventions"
|
|
||||||
class="btn btn-outline-secondary btn-sm mb-3"
|
|
||||||
>
|
|
||||||
<i class="fas fa-arrow-left me-2"></i>Retour aux interventions
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #client-detail-sidebar>
|
|
||||||
<div class="card position-sticky top-1 intervention-sidebar-card">
|
|
||||||
<div class="card-body text-center p-4">
|
|
||||||
<div class="avatar avatar-xxl position-relative mx-auto mb-3">
|
|
||||||
<div
|
|
||||||
class="intervention-avatar w-100 border-radius-xl shadow-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-notes-medical text-white text-xl"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<soft-badge color="success" variant="gradient" size="sm" class="mb-3">
|
|
||||||
Nouvelle intervention
|
|
||||||
</soft-badge>
|
|
||||||
|
|
||||||
<h5 class="mb-1">Planifier une intervention</h5>
|
|
||||||
<p class="text-sm text-muted mb-0">
|
|
||||||
Préparez une fiche claire, liée au client et au défunt, dans le
|
|
||||||
style du dashboard.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card intervention-sidebar-card mt-4">
|
|
||||||
<div class="card-header pb-0">
|
|
||||||
<h6 class="mb-1">Repères</h6>
|
|
||||||
<p class="text-sm text-muted mb-0">
|
|
||||||
Les éléments ci-dessous suffisent pour démarrer correctement.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="card-body pt-3">
|
|
||||||
<div class="check-item">
|
|
||||||
<span class="check-dot bg-gradient-success"></span>
|
|
||||||
<span class="text-sm">Associer le bon client</span>
|
|
||||||
</div>
|
|
||||||
<div class="check-item">
|
|
||||||
<span class="check-dot bg-gradient-info"></span>
|
|
||||||
<span class="text-sm">Définir le type d'intervention</span>
|
|
||||||
</div>
|
|
||||||
<div class="check-item">
|
|
||||||
<span class="check-dot bg-gradient-dark"></span>
|
|
||||||
<span class="text-sm">Renseigner la planification si connue</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #client-detail-content>
|
|
||||||
<div class="card intervention-hero-card mb-4">
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<div
|
|
||||||
class="d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-3"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
class="text-sm text-uppercase text-success font-weight-bold mb-2"
|
|
||||||
>
|
|
||||||
Dashboard intervention
|
|
||||||
</p>
|
|
||||||
<h4 class="mb-1">Créer une nouvelle intervention</h4>
|
|
||||||
<p class="text-sm text-muted mb-0">
|
|
||||||
Une mise en page plus propre, cohérente avec le reste de
|
|
||||||
l'application, centrée sur les informations métier utiles.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hero-pill-group">
|
|
||||||
<span class="hero-pill">
|
|
||||||
<i class="fas fa-user me-2 text-success"></i>Client
|
|
||||||
</span>
|
|
||||||
<span class="hero-pill">
|
|
||||||
<i class="fas fa-cross me-2 text-info"></i>Défunt
|
|
||||||
</span>
|
|
||||||
<span class="hero-pill">
|
|
||||||
<i class="fas fa-calendar-alt me-2 text-dark"></i>Planning
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<intervention-form
|
<intervention-form
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:validation-errors="validationErrors"
|
:validation-errors="validationErrors"
|
||||||
@ -107,14 +16,12 @@
|
|||||||
@create-intervention="handleCreateIntervention"
|
@create-intervention="handleCreateIntervention"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</client-detail-template>
|
</add-intervention-template>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import ClientDetailTemplate from "@/components/templates/CRM/ClientDetailTemplate.vue";
|
import AddInterventionTemplate from "@/components/templates/Interventions/AddInterventionTemplate.vue";
|
||||||
import InterventionForm from "@/components/molecules/Interventions/InterventionForm.vue";
|
import InterventionForm from "@/components/molecules/Interventions/InterventionForm.vue";
|
||||||
import SoftBadge from "@/components/SoftBadge.vue";
|
|
||||||
import { defineProps, defineEmits } from "vue";
|
import { defineProps, defineEmits } from "vue";
|
||||||
import { RouterLink } from "vue-router";
|
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
loading: {
|
loading: {
|
||||||
@ -169,57 +76,3 @@ const handleCreateIntervention = (data) => {
|
|||||||
emit("createIntervention", data);
|
emit("createIntervention", data);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.position-sticky {
|
|
||||||
top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intervention-sidebar-card,
|
|
||||||
.intervention-hero-card {
|
|
||||||
border: 0;
|
|
||||||
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intervention-avatar {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
background: linear-gradient(135deg, #17c1e8 0%, #3a416f 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.check-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check-item + .check-item {
|
|
||||||
margin-top: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check-dot {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
display: inline-block;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-pill-group {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-pill {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.7rem 1rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(248, 249, 250, 0.95);
|
|
||||||
color: #344767;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -53,9 +53,7 @@ export default {
|
|||||||
|
|
||||||
activeValue = active ? `active` : null;
|
activeValue = active ? `active` : null;
|
||||||
|
|
||||||
return [colorValue, sizeValue, fullWidthValue, activeValue]
|
return `${colorValue} ${sizeValue} ${fullWidthValue} ${activeValue}`;
|
||||||
.filter(Boolean)
|
|
||||||
.join(" ");
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<div class="form-group">
|
||||||
|
<label :for="id" class="form-label">
|
||||||
|
<i class="fas fa-paperclip"></i> Pièces jointes
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
:id="id"
|
||||||
|
type="file"
|
||||||
|
class="form-control"
|
||||||
|
multiple
|
||||||
|
@change="handleFileChange"
|
||||||
|
/>
|
||||||
|
<small class="text-muted"
|
||||||
|
>Formats acceptés: PDF, DOC, DOCX, XLS, XLSX, JPG, PNG</small
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
const props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: "webmailing-attachments",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["files-selected"]);
|
||||||
|
|
||||||
|
const handleFileChange = (event) => {
|
||||||
|
const files = event.target.files;
|
||||||
|
const fileList = Array.from(files).map((file) => ({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
}));
|
||||||
|
emit("files-selected", fileList);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea
|
||||||
|
:id="id"
|
||||||
|
v-model="localValue"
|
||||||
|
class="form-control"
|
||||||
|
:class="getClasses(error, success)"
|
||||||
|
placeholder="Contenu du message"
|
||||||
|
rows="8"
|
||||||
|
@blur="handleBlur"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, defineEmits, defineProps } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: "webmailing-body",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "blur"]);
|
||||||
|
|
||||||
|
const localValue = ref(props.modelValue);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(newValue) => {
|
||||||
|
localValue.value = newValue;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(localValue, (newValue) => {
|
||||||
|
emit("update:modelValue", newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getClasses = (error, success) => {
|
||||||
|
if (error) {
|
||||||
|
return "is-invalid";
|
||||||
|
} else if (success) {
|
||||||
|
return "is-valid";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
emit("blur");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<soft-input
|
||||||
|
:id="id"
|
||||||
|
v-model="localValue"
|
||||||
|
type="text"
|
||||||
|
placeholder="Sujet du message"
|
||||||
|
:error="error"
|
||||||
|
:success="success"
|
||||||
|
@blur="handleBlur"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineEmits, defineProps } from "vue";
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: "webmailing-subject",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "blur"]);
|
||||||
|
|
||||||
|
const localValue = ref(props.modelValue);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(newValue) => {
|
||||||
|
localValue.value = newValue;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(localValue, (newValue) => {
|
||||||
|
emit("update:modelValue", newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
emit("blur");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -1,328 +1,235 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="submitForm">
|
<div class="card p-3 border-radius-xl bg-white" data-animation="FadeIn">
|
||||||
<div class="row g-4">
|
<h5 class="font-weight-bolder mb-0">Informations de l'intervention</h5>
|
||||||
<div class="col-12 col-xl-7">
|
<p class="mb-0 text-sm">Créez une nouvelle intervention funéraire</p>
|
||||||
<div class="card intervention-panel-card h-100" data-animation="FadeIn">
|
|
||||||
<div class="card-header pb-0 p-4">
|
<div class="multisteps-form__content">
|
||||||
<div
|
<!-- Client selection -->
|
||||||
class="d-flex align-items-center justify-content-between flex-wrap gap-2"
|
<div class="row mt-3">
|
||||||
>
|
<div class="col-12">
|
||||||
<div>
|
<label class="form-label"
|
||||||
<h5 class="font-weight-bolder mb-1">
|
>Client <span class="text-danger">*</span></label
|
||||||
Informations principales
|
>
|
||||||
</h5>
|
<search-input
|
||||||
<p class="mb-0 text-sm text-muted">
|
v-model="selectedItem"
|
||||||
Identifiez le client, le défunt concerné et le type
|
:search-action="props.searchClients"
|
||||||
d'intervention.
|
:min-chars="0"
|
||||||
</p>
|
item-key="id"
|
||||||
</div>
|
item-label="name"
|
||||||
<soft-badge color="info" variant="gradient" size="sm">
|
@search="handleSearch"
|
||||||
Champs essentiels
|
@select="handleSelect"
|
||||||
</soft-badge>
|
/>
|
||||||
</div>
|
<div v-if="selectedItem" class="selected-item">
|
||||||
|
Sélectionné: {{ selectedItem.name }} ({{
|
||||||
|
selectedItem.email || "Pas d'email"
|
||||||
|
}})
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
<div class="card-body pt-3 p-4">
|
v-if="fieldErrors.client_id"
|
||||||
<div class="mb-4">
|
class="invalid-feedback small-error"
|
||||||
<label class="form-label"
|
>
|
||||||
>Client <span class="text-danger">*</span></label
|
{{ fieldErrors.client_id }}
|
||||||
>
|
|
||||||
<search-input
|
|
||||||
v-model="selectedItem"
|
|
||||||
:search-action="props.searchClients"
|
|
||||||
:min-chars="0"
|
|
||||||
item-key="id"
|
|
||||||
item-label="name"
|
|
||||||
@search="handleSearch"
|
|
||||||
@select="handleSelect"
|
|
||||||
/>
|
|
||||||
<div v-if="selectedItem" class="selection-chip mt-2">
|
|
||||||
<i class="fas fa-user me-2 text-success"></i>
|
|
||||||
<span>
|
|
||||||
{{ selectedItem.name }}
|
|
||||||
<small class="text-muted ms-1">{{
|
|
||||||
selectedItem.email || "Pas d'email"
|
|
||||||
}}</small>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="fieldErrors.client_id"
|
|
||||||
class="invalid-feedback small-error"
|
|
||||||
>
|
|
||||||
{{ fieldErrors.client_id }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="form-label">Personne décédée</label>
|
|
||||||
<search-input
|
|
||||||
v-model="selectedDeceased"
|
|
||||||
:search-action="props.searchDeceased"
|
|
||||||
:min-chars="0"
|
|
||||||
item-key="id"
|
|
||||||
:item-label="getDeceasedFullName"
|
|
||||||
@search="handleSearchDeceased"
|
|
||||||
@select="handleSelectDeceased"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="selectedDeceased"
|
|
||||||
class="selection-chip mt-2 selection-chip-muted"
|
|
||||||
>
|
|
||||||
<i class="fas fa-cross me-2 text-info"></i>
|
|
||||||
<span>
|
|
||||||
{{ selectedDeceased.last_name }}
|
|
||||||
{{ selectedDeceased.first_name || "" }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="fieldErrors.deceased_id"
|
|
||||||
class="invalid-feedback small-error"
|
|
||||||
>
|
|
||||||
{{ fieldErrors.deceased_id }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-lg-6">
|
|
||||||
<label class="form-label"
|
|
||||||
>Type d'intervention <span class="text-danger">*</span></label
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
v-model="form.type"
|
|
||||||
class="form-select soft-select"
|
|
||||||
:class="{ 'is-invalid': fieldErrors.type }"
|
|
||||||
>
|
|
||||||
<option value="">Sélectionnez un type d'intervention</option>
|
|
||||||
<option value="thanatopraxie">Thanatopraxie</option>
|
|
||||||
<option value="toilette_mortuaire">Toilette mortuaire</option>
|
|
||||||
<option value="exhumation">Exhumation</option>
|
|
||||||
<option value="retrait_pacemaker">Retrait pacemaker</option>
|
|
||||||
<option value="retrait_bijoux">Retrait bijoux</option>
|
|
||||||
<option value="autre">Autre</option>
|
|
||||||
</select>
|
|
||||||
<div
|
|
||||||
v-if="fieldErrors.type"
|
|
||||||
class="invalid-feedback small-error"
|
|
||||||
>
|
|
||||||
{{ fieldErrors.type }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-lg-6 mt-4 mt-lg-0">
|
|
||||||
<label class="form-label">Donneur d'ordre</label>
|
|
||||||
<soft-input
|
|
||||||
v-model="form.order_giver"
|
|
||||||
:class="{ 'is-invalid': fieldErrors.order_giver }"
|
|
||||||
type="text"
|
|
||||||
placeholder="Nom du donneur d'ordre"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="fieldErrors.order_giver"
|
|
||||||
class="invalid-feedback small-error"
|
|
||||||
>
|
|
||||||
{{ fieldErrors.order_giver }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-xl-5">
|
<!-- Deceased selection -->
|
||||||
<div class="card intervention-panel-card h-100">
|
<div class="row mt-3">
|
||||||
<div class="card-header pb-0 p-4">
|
<div class="col-12">
|
||||||
<div
|
<label class="form-label">Personne décédée</label>
|
||||||
class="d-flex align-items-center justify-content-between flex-wrap gap-2"
|
<search-input
|
||||||
>
|
v-model="selectedDeceased"
|
||||||
<div>
|
:search-action="props.searchDeceased"
|
||||||
<h5 class="font-weight-bolder mb-1">Planification</h5>
|
:min-chars="0"
|
||||||
<p class="mb-0 text-sm text-muted">
|
item-key="id"
|
||||||
Définissez la date, l'heure, la durée et l'état de suivi.
|
:item-label="getDeceasedFullName"
|
||||||
</p>
|
@search="handleSearchDeceased"
|
||||||
</div>
|
@select="handleSelectDeceased"
|
||||||
<soft-badge color="success" variant="gradient" size="sm">
|
/>
|
||||||
Organisation
|
<div v-if="selectedDeceased" class="selected-item">
|
||||||
</soft-badge>
|
Sélectionné: {{ selectedDeceased.last_name }}
|
||||||
</div>
|
{{ selectedDeceased.first_name || "" }}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
<div class="card-body pt-3 p-4">
|
v-if="fieldErrors.deceased_id"
|
||||||
<div class="row g-3">
|
class="invalid-feedback small-error"
|
||||||
<div class="col-12 col-sm-6">
|
>
|
||||||
<label class="form-label">Date de l'intervention</label>
|
{{ fieldErrors.deceased_id }}
|
||||||
<soft-input
|
|
||||||
v-model="form.scheduled_date"
|
|
||||||
:class="{ 'is-invalid': fieldErrors.scheduled_at }"
|
|
||||||
type="date"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-sm-6">
|
|
||||||
<label class="form-label">Heure de l'intervention</label>
|
|
||||||
<soft-input
|
|
||||||
v-model="form.scheduled_time"
|
|
||||||
:class="{ 'is-invalid': fieldErrors.scheduled_at }"
|
|
||||||
type="time"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="fieldErrors.scheduled_at"
|
|
||||||
class="col-12 invalid-feedback small-error"
|
|
||||||
>
|
|
||||||
{{ fieldErrors.scheduled_at }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-sm-6">
|
|
||||||
<label class="form-label">Durée (minutes)</label>
|
|
||||||
<soft-input
|
|
||||||
v-model="form.duration_min"
|
|
||||||
:class="{ 'is-invalid': fieldErrors.duration_min }"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
placeholder="ex. 90"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="fieldErrors.duration_min"
|
|
||||||
class="invalid-feedback small-error"
|
|
||||||
>
|
|
||||||
{{ fieldErrors.duration_min }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-sm-6">
|
|
||||||
<label class="form-label">Statut</label>
|
|
||||||
<select
|
|
||||||
v-model="form.status"
|
|
||||||
class="form-select soft-select"
|
|
||||||
:class="{ 'is-invalid': fieldErrors.status }"
|
|
||||||
>
|
|
||||||
<option value="">Sélectionnez un statut</option>
|
|
||||||
<option value="demande">Demande</option>
|
|
||||||
<option value="planifie">Planifié</option>
|
|
||||||
<option value="en_cours">En cours</option>
|
|
||||||
<option value="termine">Terminé</option>
|
|
||||||
<option value="annule">Annulé</option>
|
|
||||||
</select>
|
|
||||||
<div
|
|
||||||
v-if="fieldErrors.status"
|
|
||||||
class="invalid-feedback small-error"
|
|
||||||
>
|
|
||||||
{{ fieldErrors.status }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<!-- Intervention type -->
|
||||||
<div class="card intervention-panel-card">
|
<div class="row mt-3">
|
||||||
<div class="card-header pb-0 p-4">
|
<div class="col-12">
|
||||||
<div
|
<label class="form-label"
|
||||||
class="d-flex align-items-center justify-content-between flex-wrap gap-2"
|
>Type d'intervention <span class="text-danger">*</span></label
|
||||||
>
|
>
|
||||||
<div>
|
<select
|
||||||
<h5 class="font-weight-bolder mb-1">Observations</h5>
|
v-model="form.type"
|
||||||
<p class="mb-0 text-sm text-muted">
|
class="form-select multisteps-form__select"
|
||||||
Ajoutez le contexte utile pour l'équipe et la coordination
|
:class="{ 'is-invalid': fieldErrors.type }"
|
||||||
terrain.
|
>
|
||||||
</p>
|
<option value="">Sélectionnez un type d'intervention</option>
|
||||||
</div>
|
<option value="thanatopraxie">Thanatopraxie</option>
|
||||||
<soft-badge color="dark" variant="gradient" size="sm">
|
<option value="toilette_mortuaire">Toilette mortuaire</option>
|
||||||
Notes métier
|
<option value="exhumation">Exhumation</option>
|
||||||
</soft-badge>
|
<option value="retrait_pacemaker">Retrait pacemaker</option>
|
||||||
</div>
|
<option value="retrait_bijoux">Retrait bijoux</option>
|
||||||
</div>
|
<option value="autre">Autre</option>
|
||||||
|
</select>
|
||||||
<div class="card-body pt-3 p-4">
|
<div v-if="fieldErrors.type" class="invalid-feedback small-error">
|
||||||
<div class="row g-4 align-items-start">
|
{{ fieldErrors.type }}
|
||||||
<div class="col-12 col-xl-8">
|
|
||||||
<label class="form-label">Notes et observations</label>
|
|
||||||
<textarea
|
|
||||||
v-model="form.notes"
|
|
||||||
class="form-control soft-textarea"
|
|
||||||
:class="{ 'is-invalid': fieldErrors.notes }"
|
|
||||||
rows="5"
|
|
||||||
placeholder="Informations complémentaires, instructions spéciales..."
|
|
||||||
maxlength="2000"
|
|
||||||
></textarea>
|
|
||||||
<div
|
|
||||||
v-if="fieldErrors.notes"
|
|
||||||
class="invalid-feedback small-error"
|
|
||||||
>
|
|
||||||
{{ fieldErrors.notes }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-xl-4">
|
|
||||||
<div class="summary-panel">
|
|
||||||
<p class="summary-title mb-3">Résumé rapide</p>
|
|
||||||
<div class="summary-line">
|
|
||||||
<span>Client</span>
|
|
||||||
<strong>{{
|
|
||||||
selectedItem?.name || "Non sélectionné"
|
|
||||||
}}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="summary-line">
|
|
||||||
<span>Défunt</span>
|
|
||||||
<strong>{{
|
|
||||||
selectedDeceased
|
|
||||||
? getDeceasedFullName(selectedDeceased)
|
|
||||||
: "Non sélectionné"
|
|
||||||
}}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="summary-line">
|
|
||||||
<span>Type</span>
|
|
||||||
<strong>{{ form.type || "Non défini" }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="summary-line">
|
|
||||||
<span>Statut</span>
|
|
||||||
<strong>{{ form.status || "Non défini" }}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="button-row d-flex justify-content-end flex-wrap gap-2 mt-4"
|
|
||||||
>
|
|
||||||
<soft-button
|
|
||||||
type="button"
|
|
||||||
color="secondary"
|
|
||||||
variant="outline"
|
|
||||||
class="mb-0"
|
|
||||||
@click="resetForm"
|
|
||||||
>
|
|
||||||
Réinitialiser
|
|
||||||
</soft-button>
|
|
||||||
<soft-button
|
|
||||||
type="submit"
|
|
||||||
color="success"
|
|
||||||
variant="gradient"
|
|
||||||
class="mb-0"
|
|
||||||
:disabled="props.loading"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="props.loading"
|
|
||||||
class="spinner-border spinner-border-sm me-2"
|
|
||||||
role="status"
|
|
||||||
></span>
|
|
||||||
{{
|
|
||||||
props.loading ? "Enregistrement..." : "Créer l'intervention"
|
|
||||||
}}
|
|
||||||
</soft-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Date et heure de l'intervention -->
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 col-sm-6">
|
||||||
|
<label class="form-label">Date de l'intervention</label>
|
||||||
|
<soft-input
|
||||||
|
v-model="form.scheduled_date"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.scheduled_at }"
|
||||||
|
type="date"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="fieldErrors.scheduled_at"
|
||||||
|
class="invalid-feedback small-error"
|
||||||
|
>
|
||||||
|
{{ fieldErrors.scheduled_at }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||||
|
<label class="form-label">Heure de l'intervention</label>
|
||||||
|
<soft-input
|
||||||
|
v-model="form.scheduled_time"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.scheduled_at }"
|
||||||
|
type="time"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="fieldErrors.scheduled_at"
|
||||||
|
class="invalid-feedback small-error"
|
||||||
|
>
|
||||||
|
{{ fieldErrors.scheduled_at }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Duration and status -->
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 col-sm-6">
|
||||||
|
<label class="form-label">Durée (minutes)</label>
|
||||||
|
<soft-input
|
||||||
|
v-model="form.duration_min"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.duration_min }"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
placeholder="ex. 90"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="fieldErrors.duration_min"
|
||||||
|
class="invalid-feedback small-error"
|
||||||
|
>
|
||||||
|
{{ fieldErrors.duration_min }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||||
|
<label class="form-label">Statut</label>
|
||||||
|
<select
|
||||||
|
v-model="form.status"
|
||||||
|
class="form-select multisteps-form__select"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.status }"
|
||||||
|
>
|
||||||
|
<option value="">Sélectionnez un statut</option>
|
||||||
|
<option value="demande">Demande</option>
|
||||||
|
<option value="planifie">Planifié</option>
|
||||||
|
<option value="en_cours">En cours</option>
|
||||||
|
<option value="termine">Terminé</option>
|
||||||
|
<option value="annule">Annulé</option>
|
||||||
|
</select>
|
||||||
|
<div v-if="fieldErrors.status" class="invalid-feedback small-error">
|
||||||
|
{{ fieldErrors.status }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Order giver and notes -->
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Donneur d'ordre</label>
|
||||||
|
<soft-input
|
||||||
|
v-model="form.order_giver"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.order_giver }"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nom du donneur d'ordre"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="fieldErrors.order_giver"
|
||||||
|
class="invalid-feedback small-error"
|
||||||
|
>
|
||||||
|
{{ fieldErrors.order_giver }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Notes et observations</label>
|
||||||
|
<textarea
|
||||||
|
v-model="form.notes"
|
||||||
|
class="form-control multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.notes }"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Informations complémentaires, instructions spéciales..."
|
||||||
|
maxlength="2000"
|
||||||
|
></textarea>
|
||||||
|
<div v-if="fieldErrors.notes" class="invalid-feedback small-error">
|
||||||
|
{{ fieldErrors.notes }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Boutons -->
|
||||||
|
<div class="button-row d-flex mt-4">
|
||||||
|
<soft-button
|
||||||
|
type="button"
|
||||||
|
color="secondary"
|
||||||
|
variant="outline"
|
||||||
|
class="me-2 mb-0"
|
||||||
|
@click="resetForm"
|
||||||
|
>
|
||||||
|
Réinitialiser
|
||||||
|
</soft-button>
|
||||||
|
<soft-button
|
||||||
|
type="button"
|
||||||
|
color="dark"
|
||||||
|
variant="gradient"
|
||||||
|
class="ms-auto mb-0"
|
||||||
|
:disabled="props.loading"
|
||||||
|
@click="submitForm"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="props.loading"
|
||||||
|
class="spinner-border spinner-border-sm me-2"
|
||||||
|
role="status"
|
||||||
|
></span>
|
||||||
|
{{ props.loading ? "Enregistrement..." : "Enregistrer" }}
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||||
import SoftInput from "@/components/SoftInput.vue";
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
import SoftButton from "@/components/SoftButton.vue";
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
import SoftBadge from "@/components/SoftBadge.vue";
|
|
||||||
import SearchInput from "@/components/atoms/input/SearchInput.vue";
|
import SearchInput from "@/components/atoms/input/SearchInput.vue";
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
@ -577,15 +484,9 @@ const clearErrors = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.intervention-panel-card {
|
|
||||||
border: 0;
|
|
||||||
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-label {
|
.form-label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
color: #344767;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-danger {
|
.text-danger {
|
||||||
@ -601,21 +502,6 @@ const clearErrors = () => {
|
|||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selection-chip {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.55rem 0.85rem;
|
|
||||||
border-radius: 0.85rem;
|
|
||||||
background: rgba(45, 206, 137, 0.08);
|
|
||||||
color: #344767;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selection-chip-muted {
|
|
||||||
background: rgba(23, 193, 232, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner-border-sm {
|
.spinner-border-sm {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
@ -635,57 +521,19 @@ const clearErrors = () => {
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.soft-select,
|
.multisteps-form__select {
|
||||||
.soft-textarea {
|
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border: 1px solid #d2d6da;
|
border: 1px solid #d2d6da;
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.5rem;
|
||||||
color: #495057;
|
color: #495057;
|
||||||
padding: 0.7rem 0.9rem;
|
padding: 0.5rem 0.75rem;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
transition: all 0.15s ease-in-out;
|
transition: all 0.15s ease-in-out;
|
||||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.soft-select:focus,
|
.multisteps-form__select:focus {
|
||||||
.soft-textarea:focus {
|
|
||||||
border-color: #5e72e4;
|
border-color: #5e72e4;
|
||||||
box-shadow: 0 0 0 0.2rem rgba(94, 114, 228, 0.25);
|
box-shadow: 0 0 0 0.2rem rgba(94, 114, 228, 0.25);
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-panel {
|
|
||||||
border-radius: 1rem;
|
|
||||||
padding: 1rem;
|
|
||||||
background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%);
|
|
||||||
border: 1px solid rgba(52, 71, 103, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-title {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: #8392ab;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-line {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #67748e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-line + .summary-line {
|
|
||||||
margin-top: 0.85rem;
|
|
||||||
padding-top: 0.85rem;
|
|
||||||
border-top: 1px solid rgba(52, 71, 103, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-line strong {
|
|
||||||
color: #344767;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<div class="webmailing-form">
|
||||||
|
<div class="form-section mb-4">
|
||||||
|
<h5 class="mb-3">Destinataires</h5>
|
||||||
|
<soft-input
|
||||||
|
v-model="formData.recipients"
|
||||||
|
type="email"
|
||||||
|
placeholder="Entrez les adresses email (séparées par des virgules)"
|
||||||
|
icon="fas fa-envelope"
|
||||||
|
icon-dir="left"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section mb-4">
|
||||||
|
<h5 class="mb-3">Sujet</h5>
|
||||||
|
<webmailing-subject-input
|
||||||
|
v-model="formData.subject"
|
||||||
|
@blur="validateSubject"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section mb-4">
|
||||||
|
<h5 class="mb-3">Contenu du message</h5>
|
||||||
|
<webmailing-body-input v-model="formData.body" @blur="validateBody" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section mb-4">
|
||||||
|
<h5 class="mb-3">Pièces jointes</h5>
|
||||||
|
<webmailing-attachment @files-selected="handleFilesSelected" />
|
||||||
|
<div v-if="formData.attachments.length > 0" class="mt-3">
|
||||||
|
<h6>Fichiers sélectionnés:</h6>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li
|
||||||
|
v-for="file in formData.attachments"
|
||||||
|
:key="file.name"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
<span class="badge bg-info">{{ file.name }}</span>
|
||||||
|
<small class="ms-2 text-muted"
|
||||||
|
>({{ formatFileSize(file.size) }})</small
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h5 class="mb-3">Options</h5>
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input
|
||||||
|
id="send-copy"
|
||||||
|
v-model="formData.sendCopy"
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="send-copy">
|
||||||
|
M'envoyer une copie
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
id="send-scheduled"
|
||||||
|
v-model="formData.scheduled"
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="send-scheduled">
|
||||||
|
Programmer l'envoi
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="formData.scheduled" class="mt-3">
|
||||||
|
<soft-input
|
||||||
|
v-model="formData.scheduledDate"
|
||||||
|
type="datetime-local"
|
||||||
|
placeholder="Date et heure d'envoi"
|
||||||
|
icon="fas fa-calendar"
|
||||||
|
icon-dir="left"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineEmits, defineProps } from "vue";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
|
import WebmailingSubjectInput from "@/components/atoms/Webmailing/WebmailingSubjectInput.vue";
|
||||||
|
import WebmailingBodyInput from "@/components/atoms/Webmailing/WebmailingBodyInput.vue";
|
||||||
|
import WebmailingAttachment from "@/components/atoms/Webmailing/WebmailingAttachment.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
initialData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["form-data-change"]);
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
recipients: props.initialData.recipients || "",
|
||||||
|
subject: props.initialData.subject || "",
|
||||||
|
body: props.initialData.body || "",
|
||||||
|
attachments: props.initialData.attachments || [],
|
||||||
|
sendCopy: props.initialData.sendCopy || false,
|
||||||
|
scheduled: props.initialData.scheduled || false,
|
||||||
|
scheduledDate: props.initialData.scheduledDate || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const validateSubject = () => {
|
||||||
|
// Add validation logic if needed
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateBody = () => {
|
||||||
|
// Add validation logic if needed
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilesSelected = (files) => {
|
||||||
|
formData.value.attachments = files;
|
||||||
|
emitFormData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const emitFormData = () => {
|
||||||
|
emit("form-data-change", formData.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFileSize = (bytes) => {
|
||||||
|
if (bytes === 0) return "0 Bytes";
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.webmailing-form {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
background-color: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #17a2b8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<div class="webmailing-list">
|
||||||
|
<div v-if="emails.length === 0" class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle"></i> Aucun email envoyé
|
||||||
|
</div>
|
||||||
|
<div v-else class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Destinataires</th>
|
||||||
|
<th>Sujet</th>
|
||||||
|
<th>Date d'envoi</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="email in emails" :key="email.id">
|
||||||
|
<td>
|
||||||
|
<small>{{ email.recipients }}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>{{ email.subject }}</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<small>{{ formatDate(email.sentDate) }}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span :class="getStatusClass(email.status)">
|
||||||
|
{{ getStatusLabel(email.status) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-info" @click="viewEmail(email.id)">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-danger ms-2"
|
||||||
|
@click="deleteEmail(email.id)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
emails: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["view-email", "delete-email"]);
|
||||||
|
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return "-";
|
||||||
|
return new Date(date).toLocaleDateString("fr-FR", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusClass = (status) => {
|
||||||
|
const statusClasses = {
|
||||||
|
sent: "badge bg-success",
|
||||||
|
pending: "badge bg-warning",
|
||||||
|
failed: "badge bg-danger",
|
||||||
|
scheduled: "badge bg-info",
|
||||||
|
};
|
||||||
|
return statusClasses[status] || "badge bg-secondary";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusLabel = (status) => {
|
||||||
|
const statusLabels = {
|
||||||
|
sent: "Envoyé",
|
||||||
|
pending: "En attente",
|
||||||
|
failed: "Échoué",
|
||||||
|
scheduled: "Programmé",
|
||||||
|
};
|
||||||
|
return statusLabels[status] || "Inconnu";
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewEmail = (id) => {
|
||||||
|
emit("view-email", id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteEmail = (id) => {
|
||||||
|
emit("delete-email", id);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.webmailing-list {
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,40 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="container-fluid py-4">
|
<div class="container-fluid py-4">
|
||||||
<div class="card webmail-layout-card">
|
<slot name="webmailing-header"></slot>
|
||||||
<div class="webmail-shell__frame">
|
|
||||||
<slot name="webmailing-sidebar"></slot>
|
<div class="card shadow-lg">
|
||||||
<slot name="webmailing-list"></slot>
|
<div class="card-body">
|
||||||
<slot name="webmailing-detail"></slot>
|
<slot name="webmailing-tabs"></slot>
|
||||||
|
<slot name="webmailing-content"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script></script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.webmail-layout-card {
|
.card {
|
||||||
border: 0;
|
border: none;
|
||||||
border-radius: 1rem;
|
border-radius: 12px;
|
||||||
box-shadow: 0 20px 30px rgba(15, 23, 42, 0.08);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.webmail-shell__frame {
|
.card-body {
|
||||||
display: grid;
|
padding: 2rem;
|
||||||
grid-template-columns: 220px 320px minmax(0, 1fr);
|
|
||||||
min-height: calc(100vh - 185px);
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 1rem;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.webmail-shell__frame {
|
|
||||||
grid-template-columns: 210px 300px minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 992px) {
|
|
||||||
.webmail-shell__frame {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,3 +1,2 @@
|
|||||||
export * from "./http";
|
export * from "./http";
|
||||||
export { default as AuthService } from "./auth";
|
export { default as AuthService } from "./auth";
|
||||||
export { default as WebmailService } from "./webmail";
|
|
||||||
|
|||||||
@ -1,254 +0,0 @@
|
|||||||
import { request } from "./http";
|
|
||||||
|
|
||||||
export interface WebmailMessage {
|
|
||||||
id: number;
|
|
||||||
user_id: number;
|
|
||||||
message_uid: string | null;
|
|
||||||
direction: "incoming" | "outgoing";
|
|
||||||
folder: string;
|
|
||||||
status: string;
|
|
||||||
from_email: string | null;
|
|
||||||
from_name: string | null;
|
|
||||||
to: string[];
|
|
||||||
cc: string[];
|
|
||||||
bcc: string[];
|
|
||||||
subject: string | null;
|
|
||||||
body: string | null;
|
|
||||||
snippet: string | null;
|
|
||||||
attachments: any[];
|
|
||||||
metadata: Record<string, any>;
|
|
||||||
is_read: boolean;
|
|
||||||
is_starred: boolean;
|
|
||||||
read_at: string | null;
|
|
||||||
starred_at: string | null;
|
|
||||||
sent_at: string | null;
|
|
||||||
received_at: string | null;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebmailListMeta {
|
|
||||||
current_page: number;
|
|
||||||
last_page: number;
|
|
||||||
per_page: number;
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebmailListResponse {
|
|
||||||
data: WebmailMessage[];
|
|
||||||
meta: WebmailListMeta;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebmailItemResponse {
|
|
||||||
data: WebmailMessage;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebmailStats {
|
|
||||||
total: number;
|
|
||||||
inbox: number;
|
|
||||||
sent: number;
|
|
||||||
drafts: number;
|
|
||||||
trash: number;
|
|
||||||
unread: number;
|
|
||||||
starred: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebmailStatsResponse {
|
|
||||||
data: WebmailStats;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebmailSyncResult {
|
|
||||||
imported: number;
|
|
||||||
skipped: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebmailSyncResponse {
|
|
||||||
data: WebmailSyncResult;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebmailMailboxSettings {
|
|
||||||
id: number;
|
|
||||||
user_id: number;
|
|
||||||
imap_host: string | null;
|
|
||||||
imap_port: number | null;
|
|
||||||
imap_encryption: string | null;
|
|
||||||
imap_validate_cert: boolean;
|
|
||||||
imap_username: string | null;
|
|
||||||
imap_folder: string | null;
|
|
||||||
imap_password_configured: boolean;
|
|
||||||
smtp_host: string | null;
|
|
||||||
smtp_port: number | null;
|
|
||||||
smtp_encryption: string | null;
|
|
||||||
smtp_validate_cert: boolean;
|
|
||||||
smtp_username: string | null;
|
|
||||||
smtp_from_address: string | null;
|
|
||||||
smtp_from_name: string | null;
|
|
||||||
smtp_password_configured: boolean;
|
|
||||||
has_imap_configuration: boolean;
|
|
||||||
has_smtp_configuration: boolean;
|
|
||||||
last_synced_at: string | null;
|
|
||||||
last_sync_error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebmailMailboxSettingsResponse {
|
|
||||||
data: WebmailMailboxSettings | null;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebmailQueryParams {
|
|
||||||
folder?: string;
|
|
||||||
status?: string;
|
|
||||||
search?: string;
|
|
||||||
unread?: boolean;
|
|
||||||
starred?: boolean;
|
|
||||||
per_page?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SendWebmailPayload {
|
|
||||||
to: string[];
|
|
||||||
cc?: string[];
|
|
||||||
bcc?: string[];
|
|
||||||
subject?: string;
|
|
||||||
body: string;
|
|
||||||
folder?: string;
|
|
||||||
attachments?: any[];
|
|
||||||
metadata?: Record<string, any>;
|
|
||||||
message_uid?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateWebmailPayload {
|
|
||||||
folder?: string;
|
|
||||||
status?: string;
|
|
||||||
is_read?: boolean;
|
|
||||||
is_starred?: boolean;
|
|
||||||
subject?: string;
|
|
||||||
body?: string;
|
|
||||||
metadata?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpsertMailboxSettingsPayload {
|
|
||||||
imap_host?: string | null;
|
|
||||||
imap_port?: number | null;
|
|
||||||
imap_encryption?: string | null;
|
|
||||||
imap_validate_cert?: boolean;
|
|
||||||
imap_username?: string | null;
|
|
||||||
imap_password?: string | null;
|
|
||||||
imap_folder?: string | null;
|
|
||||||
smtp_host?: string | null;
|
|
||||||
smtp_port?: number | null;
|
|
||||||
smtp_encryption?: string | null;
|
|
||||||
smtp_validate_cert?: boolean;
|
|
||||||
smtp_username?: string | null;
|
|
||||||
smtp_password?: string | null;
|
|
||||||
smtp_from_address?: string | null;
|
|
||||||
smtp_from_name?: string | null;
|
|
||||||
clear_imap_password?: boolean;
|
|
||||||
clear_smtp_password?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WebmailService = {
|
|
||||||
async getMessages(
|
|
||||||
params: WebmailQueryParams = {}
|
|
||||||
): Promise<WebmailListResponse> {
|
|
||||||
return request<WebmailListResponse>({
|
|
||||||
url: "/api/webmail/messages",
|
|
||||||
method: "get",
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async getMessage(id: number): Promise<WebmailMessage> {
|
|
||||||
const response = await request<WebmailItemResponse>({
|
|
||||||
url: `/api/webmail/messages/${id}`,
|
|
||||||
method: "get",
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async getStats(): Promise<WebmailStats> {
|
|
||||||
const response = await request<WebmailStatsResponse>({
|
|
||||||
url: "/api/webmail/messages/stats",
|
|
||||||
method: "get",
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async sendMessage(payload: SendWebmailPayload): Promise<WebmailMessage> {
|
|
||||||
const response = await request<WebmailItemResponse>({
|
|
||||||
url: "/api/webmail/messages/send",
|
|
||||||
method: "post",
|
|
||||||
data: payload,
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async getMailboxSettings(): Promise<WebmailMailboxSettings | null> {
|
|
||||||
const response = await request<WebmailMailboxSettingsResponse>({
|
|
||||||
url: "/api/webmail/settings",
|
|
||||||
method: "get",
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateMailboxSettings(
|
|
||||||
payload: UpsertMailboxSettingsPayload
|
|
||||||
): Promise<WebmailMailboxSettings | null> {
|
|
||||||
const response = await request<WebmailMailboxSettingsResponse>({
|
|
||||||
url: "/api/webmail/settings",
|
|
||||||
method: "put",
|
|
||||||
data: payload,
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async syncMailbox(limit = 30): Promise<WebmailSyncResult> {
|
|
||||||
const response = await request<WebmailSyncResponse>({
|
|
||||||
url: "/api/webmail/messages/sync",
|
|
||||||
method: "post",
|
|
||||||
params: { limit },
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async syncMailtrap(limit = 30): Promise<WebmailSyncResult> {
|
|
||||||
const response = await request<WebmailSyncResponse>({
|
|
||||||
url: "/api/webmail/messages/sync-mailtrap",
|
|
||||||
method: "post",
|
|
||||||
params: { limit },
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateMessage(
|
|
||||||
id: number,
|
|
||||||
payload: UpdateWebmailPayload
|
|
||||||
): Promise<WebmailMessage> {
|
|
||||||
const response = await request<WebmailItemResponse>({
|
|
||||||
url: `/api/webmail/messages/${id}`,
|
|
||||||
method: "patch",
|
|
||||||
data: payload,
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteMessage(id: number): Promise<{ message: string }> {
|
|
||||||
return request<{ message: string }>({
|
|
||||||
url: `/api/webmail/messages/${id}`,
|
|
||||||
method: "delete",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WebmailService;
|
|
||||||
@ -1,279 +0,0 @@
|
|||||||
import { defineStore } from "pinia";
|
|
||||||
import { computed, ref } from "vue";
|
|
||||||
import WebmailService from "@/services/webmail";
|
|
||||||
import type {
|
|
||||||
WebmailMailboxSettings,
|
|
||||||
SendWebmailPayload,
|
|
||||||
UpdateWebmailPayload,
|
|
||||||
UpsertMailboxSettingsPayload,
|
|
||||||
WebmailMessage,
|
|
||||||
WebmailQueryParams,
|
|
||||||
WebmailSyncResult,
|
|
||||||
WebmailStats,
|
|
||||||
} from "@/services/webmail";
|
|
||||||
|
|
||||||
const emptyStats: WebmailStats = {
|
|
||||||
total: 0,
|
|
||||||
inbox: 0,
|
|
||||||
sent: 0,
|
|
||||||
drafts: 0,
|
|
||||||
trash: 0,
|
|
||||||
unread: 0,
|
|
||||||
starred: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useWebmailStore = defineStore("webmail", () => {
|
|
||||||
const loading = ref(false);
|
|
||||||
const sending = ref(false);
|
|
||||||
const syncing = ref(false);
|
|
||||||
const mailboxSettingsLoading = ref(false);
|
|
||||||
const error = ref<string | null>(null);
|
|
||||||
const messages = ref<WebmailMessage[]>([]);
|
|
||||||
const currentMessage = ref<WebmailMessage | null>(null);
|
|
||||||
const mailboxSettings = ref<WebmailMailboxSettings | null>(null);
|
|
||||||
const stats = ref<WebmailStats>({ ...emptyStats });
|
|
||||||
const pagination = ref({
|
|
||||||
current_page: 1,
|
|
||||||
last_page: 1,
|
|
||||||
per_page: 15,
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const allMessages = computed(() => messages.value);
|
|
||||||
const selectedMessage = computed(() => currentMessage.value);
|
|
||||||
const isLoading = computed(() => loading.value);
|
|
||||||
const isSending = computed(() => sending.value);
|
|
||||||
const isSyncing = computed(() => syncing.value);
|
|
||||||
const isMailboxSettingsLoading = computed(() => mailboxSettingsLoading.value);
|
|
||||||
const getStats = computed(() => stats.value);
|
|
||||||
const currentMailboxSettings = computed(() => mailboxSettings.value);
|
|
||||||
|
|
||||||
const fetchMessages = async (params: WebmailQueryParams = {}) => {
|
|
||||||
loading.value = true;
|
|
||||||
error.value = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await WebmailService.getMessages(params);
|
|
||||||
messages.value = response.data;
|
|
||||||
pagination.value = response.meta;
|
|
||||||
|
|
||||||
if (currentMessage.value) {
|
|
||||||
currentMessage.value =
|
|
||||||
response.data.find((item) => item.id === currentMessage.value?.id) ||
|
|
||||||
response.data[0] ||
|
|
||||||
null;
|
|
||||||
} else {
|
|
||||||
currentMessage.value = response.data[0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
} catch (err: any) {
|
|
||||||
error.value =
|
|
||||||
err.response?.data?.message ||
|
|
||||||
err.message ||
|
|
||||||
"Failed to fetch messages";
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchMessage = async (id: number) => {
|
|
||||||
loading.value = true;
|
|
||||||
error.value = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const message = await WebmailService.getMessage(id);
|
|
||||||
currentMessage.value = message;
|
|
||||||
messages.value = messages.value.map((item) =>
|
|
||||||
item.id === id ? message : item
|
|
||||||
);
|
|
||||||
return message;
|
|
||||||
} catch (err: any) {
|
|
||||||
error.value =
|
|
||||||
err.response?.data?.message || err.message || "Failed to fetch message";
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchStats = async () => {
|
|
||||||
try {
|
|
||||||
stats.value = await WebmailService.getStats();
|
|
||||||
return stats.value;
|
|
||||||
} catch (err: any) {
|
|
||||||
error.value =
|
|
||||||
err.response?.data?.message ||
|
|
||||||
err.message ||
|
|
||||||
"Failed to fetch webmail stats";
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendMessage = async (payload: SendWebmailPayload) => {
|
|
||||||
sending.value = true;
|
|
||||||
error.value = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const message = await WebmailService.sendMessage(payload);
|
|
||||||
messages.value = [
|
|
||||||
message,
|
|
||||||
...messages.value.filter((item) => item.id !== message.id),
|
|
||||||
];
|
|
||||||
currentMessage.value = message;
|
|
||||||
await fetchStats();
|
|
||||||
return message;
|
|
||||||
} catch (err: any) {
|
|
||||||
error.value =
|
|
||||||
err.response?.data?.message || err.message || "Failed to send message";
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
sending.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchMailboxSettings = async () => {
|
|
||||||
mailboxSettingsLoading.value = true;
|
|
||||||
error.value = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
mailboxSettings.value = await WebmailService.getMailboxSettings();
|
|
||||||
return mailboxSettings.value;
|
|
||||||
} catch (err: any) {
|
|
||||||
error.value =
|
|
||||||
err.response?.data?.message ||
|
|
||||||
err.message ||
|
|
||||||
"Failed to fetch mailbox settings";
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
mailboxSettingsLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateMailboxSettings = async (
|
|
||||||
payload: UpsertMailboxSettingsPayload
|
|
||||||
) => {
|
|
||||||
mailboxSettingsLoading.value = true;
|
|
||||||
error.value = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
mailboxSettings.value = await WebmailService.updateMailboxSettings(
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
return mailboxSettings.value;
|
|
||||||
} catch (err: any) {
|
|
||||||
error.value =
|
|
||||||
err.response?.data?.message ||
|
|
||||||
err.message ||
|
|
||||||
"Failed to update mailbox settings";
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
mailboxSettingsLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const syncMailbox = async (
|
|
||||||
params: WebmailQueryParams = {},
|
|
||||||
limit = 30
|
|
||||||
): Promise<WebmailSyncResult> => {
|
|
||||||
syncing.value = true;
|
|
||||||
error.value = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await WebmailService.syncMailbox(limit);
|
|
||||||
await Promise.all([
|
|
||||||
fetchMessages(params),
|
|
||||||
fetchStats(),
|
|
||||||
fetchMailboxSettings(),
|
|
||||||
]);
|
|
||||||
return result;
|
|
||||||
} catch (err: any) {
|
|
||||||
error.value =
|
|
||||||
err.response?.data?.message || err.message || "Failed to sync mailbox";
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
syncing.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateMessage = async (id: number, payload: UpdateWebmailPayload) => {
|
|
||||||
loading.value = true;
|
|
||||||
error.value = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const message = await WebmailService.updateMessage(id, payload);
|
|
||||||
messages.value = messages.value.map((item) =>
|
|
||||||
item.id === id ? message : item
|
|
||||||
);
|
|
||||||
if (currentMessage.value?.id === id) {
|
|
||||||
currentMessage.value = message;
|
|
||||||
}
|
|
||||||
await fetchStats();
|
|
||||||
return message;
|
|
||||||
} catch (err: any) {
|
|
||||||
error.value =
|
|
||||||
err.response?.data?.message ||
|
|
||||||
err.message ||
|
|
||||||
"Failed to update message";
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteMessage = async (id: number) => {
|
|
||||||
loading.value = true;
|
|
||||||
error.value = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await WebmailService.deleteMessage(id);
|
|
||||||
messages.value = messages.value.filter((item) => item.id !== id);
|
|
||||||
if (currentMessage.value?.id === id) {
|
|
||||||
currentMessage.value = messages.value[0] || null;
|
|
||||||
}
|
|
||||||
await fetchStats();
|
|
||||||
return response;
|
|
||||||
} catch (err: any) {
|
|
||||||
error.value =
|
|
||||||
err.response?.data?.message ||
|
|
||||||
err.message ||
|
|
||||||
"Failed to delete message";
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
loading,
|
|
||||||
sending,
|
|
||||||
syncing,
|
|
||||||
mailboxSettingsLoading,
|
|
||||||
error,
|
|
||||||
messages,
|
|
||||||
currentMessage,
|
|
||||||
mailboxSettings,
|
|
||||||
stats,
|
|
||||||
pagination,
|
|
||||||
allMessages,
|
|
||||||
selectedMessage,
|
|
||||||
isLoading,
|
|
||||||
isSending,
|
|
||||||
isSyncing,
|
|
||||||
isMailboxSettingsLoading,
|
|
||||||
getStats,
|
|
||||||
currentMailboxSettings,
|
|
||||||
fetchMessages,
|
|
||||||
fetchMessage,
|
|
||||||
fetchStats,
|
|
||||||
fetchMailboxSettings,
|
|
||||||
sendMessage,
|
|
||||||
updateMailboxSettings,
|
|
||||||
syncMailbox,
|
|
||||||
updateMessage,
|
|
||||||
deleteMessage,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default useWebmailStore;
|
|
||||||
Loading…
x
Reference in New Issue
Block a user