Task & Calendar: Xây dựng trợ lý ảo cho Môi giới Bất động sản
để hệ thống Proptech (Bất động sản) thực sự hỗ trợ được môi giới, chức năng Lịch làm việc & Nhắc lịch (Task & Calendar) chính là "trợ lý ảo" không thể thiếu.
Trong một môi trường mà một môi giới phải quản lý hàng chục khách hàng và hàng trăm căn nhà, việc chỉ lưu Task dưới dạng text đơn thuần là một sai lầm. Ở mức độ Enterprise, chúng ta cần thiết kế một hệ thống Task đa mục tiêu (Polymorphic Tasks). Nghĩa là một task có thể gắn liền với một khách hàng (Lead), một căn nhà (Listing), hoặc một hợp đồng (Contract).
Hôm nay, anh em mình sẽ xây dựng module này với tư duy: Mọi thứ phải được lên lịch và phân loại rõ ràng.
Lời mở đầu: Tại sao To-do list thông thường là chưa đủ?
Môi giới không chỉ cần biết "hôm nay làm gì", họ cần biết "làm việc đó với ai" và "về tài sản nào".
- Một lịch Site Visit (Xem nhà) phải gắn với khách hàng A và căn hộ B.
- Một lịch Contract Expiry (Hết hạn hợp đồng) phải tự động sinh ra khi hợp đồng sắp đến ngày kết thúc.
Chúng ta sẽ không thiết kế các bảng riêng lẻ cho từng loại lịch. Thay vào đó, chúng ta dùng Polymorphic Relations để tạo ra một bảng tasks duy nhất nhưng có thể "biến hình" để liên kết với bất kỳ thực thể nào trong hệ thống.
Bước 1: Khởi tạo dự án & Thiết kế Database (Polymorphic)
Tạo dự án mới:
laravel new proptech-calendar
cd proptech-calendar
1. Tạo Model và Migration cho Task:
php artisan make:model Task -m
2. Thiết kế bảng tasks:
Chúng ta cần các trường về thời gian, trạng thái và các cột đa hình (taskable).
// database/migrations/xxxx_create_tasks_table.php
public function up(): void
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete(); // Môi giới sở hữu task
$table->string('title');
$table->text('description')->nullable();
$table->string('type'); // site_visit, follow_up, contract_expiry
$table->string('status')->default('pending'); // pending, completed, cancelled
$table->string('priority')->default('medium'); // low, medium, high
$table->dateTime('due_at'); // Thời hạn hoàn thành / Giờ hẹn
$table->dateTime('remind_at')->nullable(); // Thời điểm bắn thông báo nhắc lịch
// Cột đa hình: Cho phép task liên kết với bất kỳ Model nào (Lead, Listing, Contract)
$table->nullableMorphs('taskable');
$table->timestamps();
});
}
Bước 2: Chuẩn hóa bằng PHP Enums
Sử dụng Enums để quản lý các loại công việc và trạng thái, giúp code sạch và dễ bảo trì.
// app/Enums/TaskType.php
namespace App\Enums;
enum TaskType: string {
case SITE_VISIT = 'site_visit'; // Dẫn khách xem nhà
case FOLLOW_UP = 'follow_up'; // Gọi lại cho khách
case CONTRACT_EXPIRY = 'expiry'; // Nhắc hết hạn hợp đồng
}
// app/Enums/TaskStatus.php
namespace App\Enums;
enum TaskStatus: string {
case PENDING = 'pending';
case COMPLETED = 'completed';
case CANCELLED = 'cancelled';
}
Trong Model Task.php, ta thực hiện cast dữ liệu:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Enums\TaskType;
use App\Enums\TaskStatus;
class Task extends Model
{
protected $fillable = [
'user_id', 'title', 'description', 'type', 'status',
'priority', 'due_at', 'remind_at', 'taskable_id', 'taskable_type'
];
protected $casts = [
'due_at' => 'datetime',
'remind_at' => 'datetime',
'type' => TaskType::class,
'status' => TaskStatus::class,
];
public function taskable()
{
return $this->morphTo();
}
}
Bước 3: Action Pattern - Tạo lịch hẹn thông minh
Action này sẽ giúp chúng ta xử lý việc tạo task và kiểm tra logic (ví dụ: giờ hẹn không được ở quá khứ).
// app/Actions/CreateTaskAction.php
namespace App\Actions;
use App\Models\Task;
use App\Models\User;
use Illuminate\Validation\ValidationException;
class CreateTaskAction
{
public function execute(User $user, array $data): Task
{
$dueAt = \Carbon\Carbon::parse($data['due_at']);
if ($dueAt->isPast()) {
throw ValidationException::withMessages([
'due_at' => 'Giờ hẹn không thể nằm trong quá khứ.'
]);
}
return Task::create(array_merge($data, [
'user_id' => $user->id
]));
}
}
Bước 4: API Resource - "Linh hồn" của Dashboard Agenda
Môi giới cần một cái nhìn tổng quan (Agenda) theo ngày. API Resource sẽ giúp format dữ liệu để Frontend hiển thị lên lịch biểu dễ dàng.
// app/Http/Resources/TaskResource.php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class TaskResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'type_label' => strtoupper($this->type->value),
'time' => $this->due_at->format('H:i'),
'date' => $this->due_at->format('d/m/Y'),
'status' => $this->status->value,
'priority' => $this->priority,
// Trả về thông tin của đối tượng liên quan (nếu có)
'related_to' => $this->taskable ? [
'type' => class_basename($this->taskable_type),
'name' => $this->taskable->name ?? $this->taskable->title ?? 'N/A'
] : null,
'is_overdue' => $this->due_at->isPast() && $this->status->value === 'pending',
];
}
}
Bước 5: Controller & API Agenda
Controller này cung cấp 2 tính năng: Tạo task mới và Lấy danh sách "Agenda" (Công việc trong ngày).
// app/Http/Controllers/Api/TaskController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Actions\CreateTaskAction;
use App\Models\Task;
use App\Http\Resources\TaskResource;
use Carbon\Carbon;
class TaskController extends Controller
{
/**
* Lấy danh sách công việc theo ngày (Agenda)
*/
public function agenda(Request $request)
{
$date = $request->query('date', Carbon::today()->toDateString());
$tasks = $request->user()->tasks()
->with('taskable') // Chống N+1 query
->whereDate('due_at', $date)
->orderBy('due_at', 'asc')
->get();
return TaskResource::collection($tasks);
}
/**
* Tạo lịch hẹn mới
*/
public function store(Request $request, CreateTaskAction $action)
{
$data = $request->validate([
'title' => 'required|string|max:255',
'type' => 'required|string',
'due_at' => 'required|date',
'priority' => 'string|in:low,medium,high',
'taskable_id' => 'nullable|integer',
'taskable_type' => 'nullable|string', // VD: App\Models\Listing
]);
$task = $action->execute($request->user(), $data);
return response()->json([
'success' => true,
'data' => new TaskResource($task)
], 201);
}
}
Bước 6: Thử lửa với Postman
Khởi động server: php artisan serve
- Tạo một lịch dẫn khách đi xem nhà (POST):
- URL:
http://127.0.0.1:8000/api/tasks - Body (JSON):
{
"title": "Dẫn khách xem căn hộ Landmark 81",
"type": "site_visit",
"due_at": "2026-05-07 15:30:00",
"priority": "high",
"taskable_id": 1,
"taskable_type": "App\\Models\\Listing"
}
2. Lấy lịch làm việc trong ngày (GET):
- URL:
http://127.0.0.1:8000/api/agenda?date=2026-05-07
Kết quả mong đợi:
Hệ thống sẽ trả về danh sách công việc được sắp xếp theo thời gian. Những việc đã quá giờ mà chưa hoàn thành sẽ có nhãn is_overdue: true, giúp môi giới nhận diện ngay lập tức các đầu việc bị bỏ lỡ.
All rights reserved