feat: AfterSales CRM - SOP compliant real-estate customer care system
- Departments + cross-department transfer workflow - Role-based closing (staff→resolved, manager→closed) with proof_of_resolution - Private file storage with collection system + temporary signed URLs - Dashboard: resolved tickets table, stats, handler performance bar chart - Spatie RBAC (admin/manager/staff) - Notifications: TicketTransferred, TicketClosed (database + mail) - Pest tests: 8 tests, 21 assertions - Docker production-ready (Dockerfile + compose + entrypoint)
This commit is contained in:
24
database/factories/FeedbackFactory.php
Normal file
24
database/factories/FeedbackFactory.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Feedback;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<Feedback>
|
||||
*/
|
||||
class FeedbackFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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('customer_product', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('product_id')->constrained()->cascadeOnDelete();
|
||||
$table->date('purchase_date')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['customer_id', 'product_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('customer_product');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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('customers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->nullable()->unique();
|
||||
$table->string('phone')->nullable();
|
||||
$table->text('address')->nullable();
|
||||
$table->string('status')->default('active');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('customers');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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('products', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->string('status')->default('active');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('products');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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('feedback_channels', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->string('icon')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('feedback_channels');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('feedback', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('customer_product_id')->nullable()->constrained('customer_product')->nullOnDelete();
|
||||
$table->foreignId('feedback_channel_id')->constrained('feedback_channels');
|
||||
$table->foreignId('assigned_to')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('title');
|
||||
$table->text('content');
|
||||
$table->string('status')->default('pending');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('feedback');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?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::table('users', function (Blueprint $table) {
|
||||
$table->string('role')->default('staff')->after('password');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('role');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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('feedback_interactions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('feedback_id')->constrained('feedback')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained('users');
|
||||
$table->string('type')->default('note'); // note, status_change, assignment, forward
|
||||
$table->text('content')->nullable();
|
||||
$table->json('changes')->nullable(); // tracks what changed (old_status => new_status, old_assignee => new_assignee)
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('feedback_interactions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('feedback_attachments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('feedback_id')->constrained('feedback')->cascadeOnDelete();
|
||||
$table->foreignId('feedback_interaction_id')->nullable()->constrained('feedback_interactions')->nullOnDelete();
|
||||
$table->foreignId('user_id')->constrained('users');
|
||||
$table->string('name');
|
||||
$table->string('path');
|
||||
$table->string('mime_type')->nullable();
|
||||
$table->unsignedBigInteger('size')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('feedback_attachments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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('feedback_tags', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->string('color')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('feedback_feedback_tag', function (Blueprint $table) {
|
||||
$table->foreignId('feedback_id')->constrained('feedback')->cascadeOnDelete();
|
||||
$table->foreignId('feedback_tag_id')->constrained('feedback_tags')->cascadeOnDelete();
|
||||
$table->primary(['feedback_id', 'feedback_tag_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('feedback_feedback_tag');
|
||||
Schema::dropIfExists('feedback_tags');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$teams = config('permission.teams');
|
||||
$tableNames = config('permission.table_names');
|
||||
$columnNames = config('permission.column_names');
|
||||
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
|
||||
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
|
||||
|
||||
throw_if(empty($tableNames), 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||
|
||||
/**
|
||||
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
|
||||
*/
|
||||
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
|
||||
$table->id(); // permission id
|
||||
$table->string('name');
|
||||
$table->string('guard_name');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['name', 'guard_name']);
|
||||
});
|
||||
|
||||
/**
|
||||
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
|
||||
*/
|
||||
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
|
||||
$table->id(); // role id
|
||||
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
|
||||
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
|
||||
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
|
||||
}
|
||||
$table->string('name');
|
||||
$table->string('guard_name');
|
||||
$table->timestamps();
|
||||
if ($teams || config('permission.testing')) {
|
||||
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
|
||||
} else {
|
||||
$table->unique(['name', 'guard_name']);
|
||||
}
|
||||
});
|
||||
|
||||
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
|
||||
$table->unsignedBigInteger($pivotPermission);
|
||||
|
||||
$table->string('model_type');
|
||||
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
|
||||
|
||||
$table->foreign($pivotPermission)
|
||||
->references('id') // permission id
|
||||
->on($tableNames['permissions'])
|
||||
->cascadeOnDelete();
|
||||
if ($teams) {
|
||||
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
|
||||
|
||||
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_permissions_permission_model_type_primary');
|
||||
} else {
|
||||
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_permissions_permission_model_type_primary');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
|
||||
$table->unsignedBigInteger($pivotRole);
|
||||
|
||||
$table->string('model_type');
|
||||
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
|
||||
|
||||
$table->foreign($pivotRole)
|
||||
->references('id') // role id
|
||||
->on($tableNames['roles'])
|
||||
->cascadeOnDelete();
|
||||
if ($teams) {
|
||||
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
|
||||
|
||||
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_roles_role_model_type_primary');
|
||||
} else {
|
||||
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_roles_role_model_type_primary');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
|
||||
$table->unsignedBigInteger($pivotPermission);
|
||||
$table->unsignedBigInteger($pivotRole);
|
||||
|
||||
$table->foreign($pivotPermission)
|
||||
->references('id') // permission id
|
||||
->on($tableNames['permissions'])
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->foreign($pivotRole)
|
||||
->references('id') // role id
|
||||
->on($tableNames['roles'])
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
|
||||
});
|
||||
|
||||
app('cache')
|
||||
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
|
||||
->forget(config('permission.cache.key'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$tableNames = config('permission.table_names');
|
||||
|
||||
throw_if(empty($tableNames), 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
|
||||
|
||||
Schema::dropIfExists($tableNames['role_has_permissions']);
|
||||
Schema::dropIfExists($tableNames['model_has_roles']);
|
||||
Schema::dropIfExists($tableNames['model_has_permissions']);
|
||||
Schema::dropIfExists($tableNames['roles']);
|
||||
Schema::dropIfExists($tableNames['permissions']);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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('departments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->foreignId('manager_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('departments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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::table('feedback', function (Blueprint $table) {
|
||||
$table->foreignId('current_department_id')->nullable()->after('assigned_to')->constrained('departments')->nullOnDelete();
|
||||
$table->boolean('is_escalated')->default(false)->after('current_department_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('feedback', function (Blueprint $table) {
|
||||
$table->dropForeign(['current_department_id']);
|
||||
$table->dropColumn(['current_department_id', 'is_escalated']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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('ticket_transfer_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('feedback_id')->constrained('feedback')->cascadeOnDelete();
|
||||
$table->foreignId('from_department_id')->nullable()->constrained('departments')->nullOnDelete();
|
||||
$table->foreignId('to_department_id')->nullable()->constrained('departments')->nullOnDelete();
|
||||
$table->foreignId('sender_id')->constrained('users');
|
||||
$table->text('reason');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ticket_transfer_logs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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::table('feedback_attachments', function (Blueprint $table) {
|
||||
$table->uuid('uuid')->nullable()->after('id')->unique();
|
||||
$table->string('disk')->default('local')->after('path');
|
||||
$table->string('collection')->default('general')->after('disk');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('feedback_attachments', function (Blueprint $table) {
|
||||
$table->dropColumn(['uuid', 'disk', 'collection']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?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::table('feedback_attachments', function (Blueprint $table) {
|
||||
$table->string('disk')->default('local')->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('feedback_attachments', function (Blueprint $table) {
|
||||
$table->string('disk')->default('private')->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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('notifications', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->string('type');
|
||||
$table->morphs('notifiable');
|
||||
$table->text('data');
|
||||
$table->timestamp('read_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('notifications');
|
||||
}
|
||||
};
|
||||
261
database/seeders/AfterSalesSeeder.php
Normal file
261
database/seeders/AfterSalesSeeder.php
Normal file
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\Department;
|
||||
use App\Models\Feedback;
|
||||
use App\Models\FeedbackChannel;
|
||||
use App\Models\Product;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class AfterSalesSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
Role::create(['name' => 'admin']);
|
||||
Role::create(['name' => 'manager']);
|
||||
Role::create(['name' => 'staff']);
|
||||
|
||||
$admin = User::create([
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@minicrm.local',
|
||||
'password' => bcrypt('password'),
|
||||
'role' => 'admin',
|
||||
]);
|
||||
$admin->assignRole('admin');
|
||||
|
||||
$manager = User::create([
|
||||
'name' => 'Manager',
|
||||
'email' => 'manager@minicrm.local',
|
||||
'password' => bcrypt('password'),
|
||||
'role' => 'manager',
|
||||
]);
|
||||
$manager->assignRole('manager');
|
||||
|
||||
$staff = User::create([
|
||||
'name' => 'Staff',
|
||||
'email' => 'staff@minicrm.local',
|
||||
'password' => bcrypt('password'),
|
||||
'role' => 'staff',
|
||||
]);
|
||||
$staff->assignRole('staff');
|
||||
|
||||
$depCskh = Department::create(['name' => 'Phòng CSKH', 'manager_id' => $manager->id]);
|
||||
$depKythuat = Department::create(['name' => 'Phòng Kỹ thuật', 'manager_id' => $admin->id]);
|
||||
$depKetoan = Department::create(['name' => 'Phòng Kế toán', 'manager_id' => null]);
|
||||
$depPhaply = Department::create(['name' => 'Phòng Pháp lý', 'manager_id' => null]);
|
||||
|
||||
$channels = [
|
||||
['name' => 'Email', 'slug' => 'email', 'icon' => 'heroicon-o-envelope'],
|
||||
['name' => 'Zalo', 'slug' => 'zalo', 'icon' => 'heroicon-o-chat-bubble-left'],
|
||||
['name' => 'Phone', 'slug' => 'phone', 'icon' => 'heroicon-o-phone'],
|
||||
['name' => 'Document', 'slug' => 'document', 'icon' => 'heroicon-o-document-text'],
|
||||
['name' => 'In Person', 'slug' => 'in-person', 'icon' => 'heroicon-o-user'],
|
||||
];
|
||||
|
||||
foreach ($channels as $channel) {
|
||||
FeedbackChannel::create($channel);
|
||||
}
|
||||
|
||||
$products = [
|
||||
['name' => 'Sunrise Apartment A1', 'description' => 'Luxury apartment in District 1, 3 bedrooms, river view', 'status' => 'active'],
|
||||
['name' => 'Sunrise Apartment A2', 'description' => 'Premium apartment in District 1, 2 bedrooms, city view', 'status' => 'active'],
|
||||
['name' => 'Green Valley Villa', 'description' => 'Standalone villa with private garden and pool', 'status' => 'active'],
|
||||
['name' => 'Ocean Tower Office', 'description' => 'High-end office space in central business district', 'status' => 'sold_out'],
|
||||
['name' => 'Park Hill Townhouse', 'description' => 'Modern townhouse near the park, 4 bedrooms', 'status' => 'inactive'],
|
||||
];
|
||||
|
||||
foreach ($products as $product) {
|
||||
Product::create($product);
|
||||
}
|
||||
|
||||
$customers = [
|
||||
['name' => 'Nguyen Van A', 'email' => 'nguyenvana@example.com', 'phone' => '0909123456', 'address' => 'Hanoi'],
|
||||
['name' => 'Tran Thi B', 'email' => 'tranthib@example.com', 'phone' => '0918234567', 'address' => 'Ho Chi Minh City'],
|
||||
['name' => 'Le Van C', 'email' => 'levanc@example.com', 'phone' => '0927345678', 'address' => 'Da Nang'],
|
||||
['name' => 'Pham Thi D', 'email' => 'phamthid@example.com', 'phone' => '0936456789', 'address' => 'Can Tho'],
|
||||
];
|
||||
|
||||
foreach ($customers as $customerData) {
|
||||
Customer::create($customerData);
|
||||
}
|
||||
|
||||
$customers = Customer::all();
|
||||
$products = Product::where('status', 'active')->get();
|
||||
$channels = FeedbackChannel::all();
|
||||
|
||||
$customers[0]->products()->attach($products[0]->id, ['purchase_date' => '2024-01-15']);
|
||||
$customers[0]->products()->attach($products[1]->id, ['purchase_date' => '2024-06-01']);
|
||||
$customers[1]->products()->attach($products[0]->id, ['purchase_date' => '2024-03-20']);
|
||||
$customers[2]->products()->attach($products[2]->id, ['purchase_date' => '2024-09-10']);
|
||||
$customers[3]->products()->attach($products[1]->id, ['purchase_date' => '2025-01-05']);
|
||||
$customers[3]->products()->attach($products[2]->id, ['purchase_date' => '2025-02-15']);
|
||||
|
||||
$customerProduct1 = \App\Models\CustomerProduct::where('customer_id', $customers[0]->id)->where('product_id', $products[0]->id)->first();
|
||||
$customerProduct2 = \App\Models\CustomerProduct::where('customer_id', $customers[0]->id)->where('product_id', $products[1]->id)->first();
|
||||
$customerProduct3 = \App\Models\CustomerProduct::where('customer_id', $customers[1]->id)->where('product_id', $products[0]->id)->first();
|
||||
|
||||
$feedbacks = [
|
||||
[
|
||||
'customer_id' => $customers[0]->id,
|
||||
'customer_product_id' => $customerProduct1->id,
|
||||
'feedback_channel_id' => $channels->firstWhere('slug', 'email')->id,
|
||||
'title' => 'Leaking ceiling in living room',
|
||||
'content' => 'I noticed water stains on the ceiling of the living room after heavy rain. Please send someone to inspect and fix.',
|
||||
'status' => 'pending',
|
||||
'assigned_to' => $staff->id,
|
||||
'current_department_id' => $depCskh->id,
|
||||
],
|
||||
[
|
||||
'customer_id' => $customers[0]->id,
|
||||
'customer_product_id' => $customerProduct2->id,
|
||||
'feedback_channel_id' => $channels->firstWhere('slug', 'phone')->id,
|
||||
'title' => 'Request for parking slot upgrade',
|
||||
'content' => 'I would like to upgrade from one parking slot to two. Please advise on availability and pricing.',
|
||||
'status' => 'processing',
|
||||
'assigned_to' => $manager->id,
|
||||
'current_department_id' => $depCskh->id,
|
||||
],
|
||||
[
|
||||
'customer_id' => $customers[1]->id,
|
||||
'customer_product_id' => $customerProduct3->id,
|
||||
'feedback_channel_id' => $channels->firstWhere('slug', 'zalo')->id,
|
||||
'title' => 'AC not cooling properly',
|
||||
'content' => 'The air conditioner in the master bedroom is making loud noises and not cooling effectively.',
|
||||
'status' => 'resolved',
|
||||
'assigned_to' => $staff->id,
|
||||
'current_department_id' => $depKythuat->id,
|
||||
],
|
||||
[
|
||||
'customer_id' => $customers[2]->id,
|
||||
'customer_product_id' => null,
|
||||
'feedback_channel_id' => $channels->firstWhere('slug', 'document')->id,
|
||||
'title' => 'General feedback on community services',
|
||||
'content' => 'I suggest improving the garbage collection schedule to twice a day instead of once. Also, the security guard shift changes could be more organized.',
|
||||
'status' => 'pending',
|
||||
'assigned_to' => null,
|
||||
'current_department_id' => $depCskh->id,
|
||||
],
|
||||
[
|
||||
'customer_id' => $customers[3]->id,
|
||||
'customer_product_id' => \App\Models\CustomerProduct::where('customer_id', $customers[3]->id)->where('product_id', $products[1]->id)->first()->id,
|
||||
'feedback_channel_id' => $channels->firstWhere('slug', 'email')->id,
|
||||
'title' => 'Kitchen cabinet hinge broken',
|
||||
'content' => 'One of the kitchen cabinet hinges is broken. It is still under warranty, please arrange replacement.',
|
||||
'status' => 'closed',
|
||||
'assigned_to' => $staff->id,
|
||||
'current_department_id' => $depKythuat->id,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($feedbacks as $feedback) {
|
||||
Feedback::create($feedback);
|
||||
}
|
||||
|
||||
$tags = [
|
||||
['name' => 'Leaking', 'slug' => 'leaking', 'color' => '#3b82f6'],
|
||||
['name' => 'Electrical', 'slug' => 'electrical', 'color' => '#f59e0b'],
|
||||
['name' => 'Warranty', 'slug' => 'warranty', 'color' => '#10b981'],
|
||||
['name' => 'Maintenance', 'slug' => 'maintenance', 'color' => '#8b5cf6'],
|
||||
['name' => 'Service Request', 'slug' => 'service-request', 'color' => '#ec4899'],
|
||||
];
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
\App\Models\FeedbackTag::create($tag);
|
||||
}
|
||||
|
||||
$allFeedback = Feedback::all();
|
||||
$allTags = \App\Models\FeedbackTag::all();
|
||||
|
||||
$allFeedback[0]->tags()->attach([$allTags[0]->id, $allTags[3]->id]);
|
||||
$allFeedback[1]->tags()->attach([$allTags[4]->id]);
|
||||
$allFeedback[2]->tags()->attach([$allTags[1]->id, $allTags[2]->id]);
|
||||
$allFeedback[3]->tags()->attach([$allTags[3]->id]);
|
||||
$allFeedback[4]->tags()->attach([$allTags[2]->id, $allTags[3]->id]);
|
||||
|
||||
\App\Models\FeedbackInteraction::create([
|
||||
'feedback_id' => $allFeedback[0]->id,
|
||||
'user_id' => $admin->id,
|
||||
'type' => 'note',
|
||||
'content' => 'Feedback received via Email. Assigned to Staff for initial handling.',
|
||||
'changes' => ['status' => ['old' => null, 'new' => 'pending']],
|
||||
]);
|
||||
|
||||
\App\Models\FeedbackInteraction::create([
|
||||
'feedback_id' => $allFeedback[0]->id,
|
||||
'user_id' => $staff->id,
|
||||
'type' => 'note',
|
||||
'content' => 'Contacted customer via phone. Scheduled inspection for next Tuesday. Customer agreed.',
|
||||
]);
|
||||
|
||||
\App\Models\FeedbackInteraction::create([
|
||||
'feedback_id' => $allFeedback[2]->id,
|
||||
'user_id' => $staff->id,
|
||||
'type' => 'status_change',
|
||||
'content' => 'Technician visited site on 2024-06-10. AC repaired. Customer confirmed cooling is now normal.',
|
||||
'changes' => ['status' => ['old' => 'processing', 'new' => 'resolved']],
|
||||
]);
|
||||
|
||||
\App\Models\FeedbackInteraction::create([
|
||||
'feedback_id' => $allFeedback[1]->id,
|
||||
'user_id' => $manager->id,
|
||||
'type' => 'note',
|
||||
'content' => 'Parking upgrade available. Sent quotation via email. Waiting for customer confirmation.',
|
||||
]);
|
||||
|
||||
\App\Models\FeedbackInteraction::create([
|
||||
'feedback_id' => $allFeedback[4]->id,
|
||||
'user_id' => $staff->id,
|
||||
'type' => 'status_change',
|
||||
'content' => 'Replacement hinge installed on 2025-03-01. All kitchen cabinets now working properly.',
|
||||
'changes' => ['status' => ['old' => 'processing', 'new' => 'closed']],
|
||||
]);
|
||||
|
||||
\App\Models\TicketTransferLog::create([
|
||||
'feedback_id' => $allFeedback[2]->id,
|
||||
'from_department_id' => $depCskh->id,
|
||||
'to_department_id' => $depKythuat->id,
|
||||
'sender_id' => $manager->id,
|
||||
'reason' => 'Vấn đề kỹ thuật về điều hòa, cần Phòng Kỹ thuật kiểm tra và xử lý.',
|
||||
]);
|
||||
|
||||
\App\Models\FeedbackAttachment::create([
|
||||
'uuid' => \Illuminate\Support\Str::uuid(),
|
||||
'feedback_id' => $allFeedback[2]->id,
|
||||
'user_id' => $staff->id,
|
||||
'name' => 'ac_repair_report.pdf',
|
||||
'path' => 'feedback-attachments/ac_repair_report.pdf',
|
||||
'disk' => 'local',
|
||||
'collection' => 'proof_of_resolution',
|
||||
'mime_type' => 'application/pdf',
|
||||
'size' => 204800,
|
||||
]);
|
||||
|
||||
\App\Models\FeedbackAttachment::create([
|
||||
'uuid' => \Illuminate\Support\Str::uuid(),
|
||||
'feedback_id' => $allFeedback[0]->id,
|
||||
'user_id' => $staff->id,
|
||||
'name' => 'ceiling_photo.jpg',
|
||||
'path' => 'feedback-attachments/ceiling_photo.jpg',
|
||||
'disk' => 'local',
|
||||
'collection' => 'customer_evidence',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'size' => 512000,
|
||||
]);
|
||||
|
||||
\App\Models\FeedbackAttachment::create([
|
||||
'uuid' => \Illuminate\Support\Str::uuid(),
|
||||
'feedback_id' => $allFeedback[4]->id,
|
||||
'user_id' => $staff->id,
|
||||
'name' => 'replaced_hinge_photo.png',
|
||||
'path' => 'feedback-attachments/replaced_hinge_photo.png',
|
||||
'disk' => 'local',
|
||||
'collection' => 'proof_of_resolution',
|
||||
'mime_type' => 'image/png',
|
||||
'size' => 307200,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
@@ -10,16 +9,10 @@ class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
$this->call([
|
||||
AfterSalesSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user