feat(portal): lead module, WHMCS sync report/contacts/mapping, service lifecycle email templates
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
3c7710f1de
commit
a443ec3bf6
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Lead\App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Lead\App\Http\Requests\LeadRequest;
|
||||
use Modules\Lead\App\Http\Resources\LeadResource;
|
||||
use Modules\Lead\App\Models\Lead;
|
||||
|
||||
class LeadController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$leads = Lead::query()
|
||||
->with('customer:id,uuid,fullname,email')
|
||||
->search($request->input('filter'))
|
||||
->when($request->filled('status'), fn ($query) => $query->where('status', $request->input('status')))
|
||||
->when($request->filled('source'), fn ($query) => $query->where('source', $request->input('source')))
|
||||
->when($request->input('linked') === '1', fn ($query) => $query->whereNotNull('customer_id'))
|
||||
->when($request->input('linked') === '0', fn ($query) => $query->whereNull('customer_id'))
|
||||
->orderByDesc('id')
|
||||
->paginate(min(max((int) $request->input('per_page', 20), 1), 100));
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'items' => LeadResource::collection($leads->items()),
|
||||
'total' => $leads->total(),
|
||||
'per_page' => $leads->perPage(),
|
||||
'current_page' => $leads->currentPage(),
|
||||
'last_page' => $leads->lastPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(string $uuid): JsonResponse
|
||||
{
|
||||
return response()->json(['error' => false, 'data' => new LeadResource($this->find($uuid)->load('customer:id,uuid,fullname,email'))]);
|
||||
}
|
||||
|
||||
public function store(LeadRequest $request): JsonResponse
|
||||
{
|
||||
$lead = Lead::create($request->validated());
|
||||
|
||||
return response()->json(['error' => false, 'message' => 'Đã thêm khách tiềm năng.', 'data' => new LeadResource($lead->load('customer:id,uuid,fullname,email'))], 201);
|
||||
}
|
||||
|
||||
public function update(string $uuid, LeadRequest $request): JsonResponse
|
||||
{
|
||||
$lead = $this->find($uuid);
|
||||
$lead->update($request->validated());
|
||||
|
||||
return response()->json(['error' => false, 'message' => 'Đã cập nhật khách tiềm năng.', 'data' => new LeadResource($lead->load('customer:id,uuid,fullname,email'))]);
|
||||
}
|
||||
|
||||
public function destroy(string $uuid): JsonResponse
|
||||
{
|
||||
$this->find($uuid)->delete();
|
||||
|
||||
return response()->json(['error' => false, 'message' => 'Đã xoá khách tiềm năng.']);
|
||||
}
|
||||
|
||||
public function bulkDestroy(Request $request): JsonResponse
|
||||
{
|
||||
$uuids = $this->bulkUuids($request);
|
||||
$deleted = Lead::query()->whereIn('uuid', $uuids)->delete();
|
||||
|
||||
return response()->json(['error' => false, 'message' => "Đã xoá {$deleted} khách tiềm năng.", 'data' => ['affected' => $deleted]]);
|
||||
}
|
||||
|
||||
public function bulkStatus(Request $request): JsonResponse
|
||||
{
|
||||
$status = $request->validate(['status' => ['required', Rule::in(Lead::STATUSES)]])['status'];
|
||||
$updated = Lead::query()->whereIn('uuid', $this->bulkUuids($request))->update(['status' => $status, 'updated_at' => now()]);
|
||||
|
||||
return response()->json(['error' => false, 'message' => "Đã cập nhật trạng thái cho {$updated} khách tiềm năng.", 'data' => ['affected' => $updated]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function bulkUuids(Request $request): array
|
||||
{
|
||||
return $request->validate([
|
||||
'uuids' => ['required', 'array', 'min:1', 'max:500'],
|
||||
'uuids.*' => ['required', 'string', 'max:36'],
|
||||
])['uuids'];
|
||||
}
|
||||
|
||||
private function find(string $uuid): Lead
|
||||
{
|
||||
return Lead::query()->where('uuid', $uuid)->firstOrFail();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Lead\App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Lead\App\Models\Lead;
|
||||
|
||||
class LeadRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'fullname' => ['nullable', 'string', 'max:255'],
|
||||
'company' => ['nullable', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255', Rule::unique('leads', 'email')->ignore($this->route('uuid'), 'uuid')],
|
||||
'phone' => ['nullable', 'string', 'max:30'],
|
||||
'address' => ['nullable', 'string', 'max:1000'],
|
||||
'status' => ['nullable', Rule::in(Lead::STATUSES)],
|
||||
'customer_id' => ['nullable', 'integer', 'exists:customers,id'],
|
||||
'receives_invoice_emails' => ['nullable', 'boolean'],
|
||||
'receives_domain_emails' => ['nullable', 'boolean'],
|
||||
'receives_support_emails' => ['nullable', 'boolean'],
|
||||
'receives_product_emails' => ['nullable', 'boolean'],
|
||||
'receives_general_emails' => ['nullable', 'boolean'],
|
||||
'note' => ['nullable', 'string', 'max:5000'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'email.required' => 'Email là bắt buộc.',
|
||||
'email.unique' => 'Email này đã có trong danh sách khách tiềm năng.',
|
||||
];
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
if ($this->has('email')) {
|
||||
$this->merge(['email' => strtolower(trim((string) $this->input('email')))]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Lead\App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class LeadResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'uuid' => $this->uuid,
|
||||
'fullname' => $this->fullname,
|
||||
'company' => $this->company,
|
||||
'email' => $this->email,
|
||||
'phone' => $this->phone,
|
||||
'address' => $this->address,
|
||||
'source' => $this->source,
|
||||
'status' => $this->status,
|
||||
'whmcs_client_id' => $this->whmcs_client_id,
|
||||
'receives_invoice_emails' => $this->receives_invoice_emails,
|
||||
'receives_domain_emails' => $this->receives_domain_emails,
|
||||
'receives_support_emails' => $this->receives_support_emails,
|
||||
'receives_product_emails' => $this->receives_product_emails,
|
||||
'receives_general_emails' => $this->receives_general_emails,
|
||||
'note' => $this->note,
|
||||
'customer' => $this->whenLoaded('customer', fn () => $this->customer ? [
|
||||
'id' => $this->customer->id,
|
||||
'uuid' => $this->customer->uuid,
|
||||
'fullname' => $this->customer->fullname,
|
||||
'email' => $this->customer->email,
|
||||
] : null),
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Lead\App\Models;
|
||||
|
||||
use App\Models\Customer;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Lead extends Model
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
public const STATUSES = ['new', 'contacted', 'converted', 'lost'];
|
||||
|
||||
public const SOURCES = ['manual', 'whmcs_contact'];
|
||||
|
||||
protected $fillable = [
|
||||
'legacy_id',
|
||||
'customer_id',
|
||||
'whmcs_client_id',
|
||||
'fullname',
|
||||
'company',
|
||||
'email',
|
||||
'phone',
|
||||
'address',
|
||||
'source',
|
||||
'status',
|
||||
'receives_invoice_emails',
|
||||
'receives_domain_emails',
|
||||
'receives_support_emails',
|
||||
'receives_product_emails',
|
||||
'receives_general_emails',
|
||||
'note',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'receives_invoice_emails' => 'boolean',
|
||||
'receives_domain_emails' => 'boolean',
|
||||
'receives_support_emails' => 'boolean',
|
||||
'receives_product_emails' => 'boolean',
|
||||
'receives_general_emails' => 'boolean',
|
||||
];
|
||||
|
||||
public function uniqueIds(): array
|
||||
{
|
||||
return ['uuid'];
|
||||
}
|
||||
|
||||
public function customer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Customer::class);
|
||||
}
|
||||
|
||||
public function scopeSearch(Builder $query, ?string $term): Builder
|
||||
{
|
||||
$term = trim((string) $term);
|
||||
|
||||
return $term === '' ? $query : $query->where(function (Builder $query) use ($term) {
|
||||
$like = '%' . addcslashes($term, '%_\\') . '%';
|
||||
$query->where('fullname', 'like', $like)
|
||||
->orWhere('company', 'like', $like)
|
||||
->orWhere('email', 'like', $like)
|
||||
->orWhere('phone', 'like', $like);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Lead\App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class LeadServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
$this->mergeConfigFrom(module_path('Lead', 'config/config.php'), 'lead');
|
||||
$this->loadMigrationsFrom(module_path('Lead', 'Database/migrations'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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('leads', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->uuid('uuid')->unique();
|
||||
$table->unsignedBigInteger('legacy_id')->nullable()->unique();
|
||||
$table->foreignId('customer_id')->nullable()->constrained('customers')->nullOnDelete();
|
||||
$table->unsignedBigInteger('whmcs_client_id')->nullable();
|
||||
$table->string('fullname')->nullable();
|
||||
$table->string('company')->nullable();
|
||||
$table->string('email')->unique();
|
||||
$table->string('phone', 30)->nullable();
|
||||
$table->text('address')->nullable();
|
||||
$table->string('source', 30)->default('manual');
|
||||
$table->string('status', 20)->default('new')->index();
|
||||
$table->boolean('receives_invoice_emails')->default(false);
|
||||
$table->boolean('receives_domain_emails')->default(false);
|
||||
$table->boolean('receives_support_emails')->default(false);
|
||||
$table->boolean('receives_product_emails')->default(false);
|
||||
$table->boolean('receives_general_emails')->default(false);
|
||||
$table->text('note')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('leads');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "nwidart/lead",
|
||||
"description": "",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Widart",
|
||||
"email": "n.widart@gmail.com"
|
||||
}
|
||||
],
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [],
|
||||
"aliases": {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\Lead\\": "",
|
||||
"Modules\\Lead\\App\\": "App/",
|
||||
"Modules\\Lead\\Database\\Factories\\": "Database/Factories/",
|
||||
"Modules\\Lead\\Database\\Seeders\\": "Database/Seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Modules\\Lead\\Tests\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'Lead',
|
||||
];
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "Lead",
|
||||
"alias": "lead",
|
||||
"description": "Khach tiem nang va nguoi lien he chua phai khach hang",
|
||||
"keywords": [],
|
||||
"priority": 0,
|
||||
"providers": [
|
||||
"Modules\\Lead\\App\\Providers\\LeadServiceProvider"
|
||||
],
|
||||
"files": []
|
||||
}
|
||||
Reference in New Issue
Block a user