Merge branch 'feature/DatabaseDesign' into 'develop'

Feature/database design

See merge request witel-back-end/crm!13
This commit is contained in:
2025-07-23 06:22:42 +00:00
34 changed files with 651 additions and 35 deletions

View File

@@ -23,10 +23,8 @@ class UserManagementController extends Controller
public function index(Request $request): JsonResponse
{
$query = User::query()->where('username', "<>", "witel");
$data = DataTableFacade::run(
$query,
User::query()->where('username', "<>", "witel"),
$request,
allowedFilters: array('id', 'full_name', 'username', 'province_id', 'phone_number', 'national_id', 'telephone_id'),
allowedSortings: array('id', 'full_name', 'username', 'province_id', 'phone_number', 'national_id', 'telephone_id'),
@@ -41,10 +39,6 @@ class UserManagementController extends Controller
DB::transaction(function () use ($request) {
$province = Province::query()->find($request->province_id);
$avatar = $request->avatar
? FileFacade::save($request->file('avatar'), 'users/avatars/' . $request->username)
: null;
$user = User::query()->create([
'full_name' => $request->full_name,
'username' => $request->username,
@@ -55,7 +49,6 @@ class UserManagementController extends Controller
'province_id' => $province->id,
'province_fa' => $province->name,
'password' => Hash::make($request->password),
'avatar' => $avatar,
'email' => $request->email,
'telephone_id' => $request->telephone_id,
]);
@@ -86,6 +79,7 @@ class UserManagementController extends Controller
'province_id' => $province->id,
'province_fa' => $province->name,
'email' => $request->email,
'telephone_id' => $request->telephone_id,
]);
$user->syncRoles(Role::query()->find($request->role_id));

View File

@@ -33,15 +33,11 @@ class AuthController extends Controller
{
Auth::user()->update([
'last_login' => now(),
'telephone_id' => $request->telephone_id,
]);
}
public function logout(Request $request): JsonResponse
{
Auth::user()->update([
'telephone_id' => null,
]);
Auth::guard()->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();

View File

@@ -105,7 +105,7 @@ class CallController extends Controller
$event_name = Event::query()->find($event_id)->first()->name;
CallHistory::create([
CallHistory::query()->create([
'call_id' => $call->id,
'telephone_id' => $call->telephone_id,
'caller_phone_number' => $call->caller_phone_number,

View File

@@ -21,6 +21,7 @@ class ProfileController extends Controller
{
return $this->successResponse(new UserResource(auth()->user()));
}
public function changePassword(ChangePasswordRequest $request): JsonResponse
{
$user = auth()->user();

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Setad;
use App\Facades\DataTable\DataTableFacade;
use App\Http\Controllers\Controller;
use App\Models\CountryCallsReport;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CumulativeCentersStatsController extends Controller
{
public function index(Request $request): JsonResponse
{
$data = DataTableFacade::run(
CountryCallsReport::query(),
$request,
allowedFilters: ['*'],
allowedSortings: ['*'],
);
return response()->json($data);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Setad;
use App\Facades\DataTable\DataTableFacade;
use App\Http\Controllers\Controller;
use App\Models\CountryKeypressReport;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CumulativeKeypressStatsController extends Controller
{
public function index(Request $request): JsonResponse
{
$data = DataTableFacade::run(
CountryKeypressReport::query(),
$request,
allowedFilters: ['*'],
allowedSortings: ['*'],
);
return response()->json($data);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Setad;
use App\Facades\DataTable\DataTableFacade;
use App\Http\Controllers\Controller;
use App\Models\CountryVoiceRatingReport;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CumulativeVoiceRatingStatsController extends Controller
{
public function index(Request $request): JsonResponse
{
$data = DataTableFacade::run(
CountryVoiceRatingReport::query(),
$request,
allowedFilters: ['*'],
allowedSortings: ['*'],
);
return response()->json($data);
}
}

View File

@@ -26,14 +26,14 @@ class StoreRequest extends FormRequest
'full_name' => 'required|max:255|string',
'username' => 'required|unique:users,username|string',
'position' => 'required',
'gender' => 'required|in:male,female',
'gender' => 'required|in:1,2',
'national_id' => 'required|digits:10|unique:users,national_id',
'phone_number' => 'required|numeric|digits:11|unique:users,phone_number|regex:/^09\d{9}$/',
'province_id' => 'required|exists:provinces,id',
'password' => 'required|regex:/^(?=.*[a-zA-Z])(?=.*\d).+$/|min:8',
'role_id' => 'required|exists:roles,id',
'avatar' => 'mimes:png,jpg,pdf|max:2048',
'email' => 'email|unique:users,email',
'telephone_id' => 'required|numeric|unique:users,telephone_id',
];
}
}

View File

@@ -39,6 +39,7 @@ class UpdateRequest extends FormRequest
'role_id' => 'required|exists:roles,id',
'avatar' => 'mimes:png,jpg,pdf|max:2048',
'email' => ['email', Rule::unique('users', 'email')->ignore($this->user->id)],
'telephone_id' => ['required', 'numeric', Rule::unique('users', 'telephone_id')->ignore($this->user->id)],
];
}
}

View File

@@ -27,25 +27,6 @@ class LoginRequest extends FormRequest
return [
'username' => 'required|exists:users,username',
'password' => 'required|string',
'telephone_id' => 'required|string'
];
}
public function after(): array
{
return [
function (Validator $validator) {
$user = User::query()->where('telephone_id', '=', $this->telephone_id);
if ($user->exists()) {
if ($user->first()->is_online) {
$validator->errors()->add(
'telephone_id',
__('messages.telephone_id_is_in_use')
);
}
$user->first()->update(['telephone_id' => null]);
}
}
];
}
}

View File

@@ -26,6 +26,7 @@ class UserResource extends JsonResource
'province_id' => $this->province_id,
'province_name' => $this->province_name,
'role' => $this->roles->pluck('id', 'name_fa'),
'permissions' => $this->getPermissionNames(),
'telephone_id' => $this->telephone_id
];
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CountryCallsReport extends Model
{
/** @use HasFactory<\Database\Factories\CountryCallsReportFactory> */
use HasFactory;
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CountryKeypressReport extends Model
{
/** @use HasFactory<\Database\Factories\CountryKeypressReportFactory> */
use HasFactory;
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CountryMessageRatingReport extends Model
{
/** @use HasFactory<\Database\Factories\CountryMessageRatingReportFactory> */
use HasFactory;
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CountryVoiceRatingReport extends Model
{
/** @use HasFactory<\Database\Factories\CountryVoiceRatingReportFactory> */
use HasFactory;
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ExpertPerformance extends Model
{
/** @use HasFactory<\Database\Factories\ExpertPerformanceFactory> */
use HasFactory;
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Database\Factories;
use App\Models\Province;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\CountryCallsReport>
*/
class CountryCallsReportFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
// $this->faker->randomElement(Province::all()->pluck('id')->toArray())
return [
'province_id' => Province::factory(),
'province_name' => function (array $attributes) {
return Province::query()->find($attributes['province_id'])->name;
},
'date' => $this->faker->date(),
'total' => $this->faker->numberBetween(1000, 10000),
'route_to_teh4' => $this->faker->numberBetween(10, 100),
'route_to_teh5' => $this->faker->numberBetween(10, 100),
'route_to_os4' => $this->faker->numberBetween(10, 100),
'route_to_os5' => $this->faker->numberBetween(10, 100),
'answered' => $this->faker->numberBetween(10, 100),
'answered_over10' => $this->faker->numberBetween(10, 100),
'missed' => $this->faker->numberBetween(10, 100),
'dnd' => $this->faker->numberBetween(10, 100),
'transferred' => $this->faker->numberBetween(10, 100),
'talked_to_operator' => $this->faker->numberBetween(10, 100),
'average_calls_time' => $this->faker->numberBetween(10, 100),
'fails' => $this->faker->numberBetween(10, 100),
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Database\Factories;
use App\Models\Province;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\CountryKeypressReport>
*/
class CountryKeypressReportFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
// $this->faker->randomElement(Province::all()->pluck('id')->toArray())
return [
'province_id' => Province::factory(),
'province_name' => function (array $attributes) {
return Province::query()->find($attributes['province_id'])->name;
},
'date' => $this->faker->date(),
'total' => $this->faker->numberBetween(1000, 10000),
'key1' => $this->faker->numberBetween(10, 100),
'key2' => $this->faker->numberBetween(10, 100),
'key3' => $this->faker->numberBetween(10, 100),
'key4' => $this->faker->numberBetween(10, 100),
'key5' => $this->faker->numberBetween(10, 100),
'key6' => $this->faker->numberBetween(10, 100),
'key9' => $this->faker->numberBetween(10, 100),
'key4_1' => $this->faker->numberBetween(10, 100),
'key4_2' => $this->faker->numberBetween(10, 100),
'key5_1' => $this->faker->numberBetween(10, 100),
'key5_2' => $this->faker->numberBetween(10, 100),
'key2_1' => $this->faker->numberBetween(10, 100),
'key2_2' => $this->faker->numberBetween(10, 100),
'key2_3' => $this->faker->numberBetween(10, 100),
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Database\Factories;
use App\Models\Province;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\CountryMessageRatingReport>
*/
class CountryMessageRatingReportFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
// $this->faker->randomElement(Province::all()->pluck('id')->toArray())
return [
'province_id' => Province::factory(),
'province_name' => function (array $attributes) {
return Province::query()->find($attributes['province_id'])->name;
},
'date' => $this->faker->date(),
'very_good' => $this->faker->numberBetween(10, 100),
'good' => $this->faker->numberBetween(10, 100),
'average' => $this->faker->numberBetween(10, 100),
'bad' => $this->faker->numberBetween(10, 100),
'very_bad' => $this->faker->numberBetween(10, 100),
'wrong_code' => $this->faker->numberBetween(10, 100),
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Database\Factories;
use App\Models\Province;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\CountryVoiceRatingReport>
*/
class CountryVoiceRatingReportFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
// $this->faker->randomElement(Province::all()->pluck('id')->toArray())
return [
'province_id' => Province::factory(),
'province_name' => function (array $attributes) {
return Province::query()->find($attributes['province_id'])->name;
},
'date' => $this->faker->date(),
'voice_quality_ok' => $this->faker->numberBetween(10, 100),
'message_quality_ok' => $this->faker->numberBetween(10, 100),
'voice_format_ok' => $this->faker->numberBetween(10, 100),
'voice_quality_nok' => $this->faker->numberBetween(10, 100),
'message_quality_nok' => $this->faker->numberBetween(10, 100),
'voice_format_nok' => $this->faker->numberBetween(10, 100),
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ExpertPerformance>
*/
class ExpertPerformanceFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
//
];
}
}

View File

@@ -17,9 +17,11 @@ class ProvinceFactory extends Factory
public function definition(): array
{
return [
'name' => $this->faker->state,
'name' => $this->faker->name(),
'lat' => $this->faker->latitude,
'lon' => $this->faker->longitude,
'ip' => $this->faker->ipv4,
'camera_ip' => $this->faker->ipv4,
];
}
}

View File

@@ -0,0 +1,43 @@
<?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
{
Schema::create('country_calls_reports', function (Blueprint $table) {
$table->id();
$table->foreignId('province_id')->constrained('provinces');
$table->string('province_name');
$table->date('date');
$table->integer('total');
$table->integer('route_to_teh4');
$table->integer('route_to_teh5');
$table->integer('route_to_os4');
$table->integer('route_to_os5');
$table->integer('answered');
$table->integer('answered_over10');
$table->integer('missed');
$table->integer('dnd');
$table->integer('transferred');
$table->integer('talked_to_operator');
$table->float('average_calls_time');
$table->integer('fails');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('country_calls_reports');
}
};

View File

@@ -0,0 +1,45 @@
<?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
{
Schema::create('country_keypress_reports', function (Blueprint $table) {
$table->id();
$table->foreignId('province_id')->constrained('provinces');
$table->string('province_name');
$table->date('date');
$table->integer('total');
$table->integer('key1');
$table->integer('key2');
$table->integer('key3');
$table->integer('key4');
$table->integer('key5');
$table->integer('key6');
$table->integer('key9');
$table->integer('key4_1');
$table->integer('key4_2');
$table->integer('key5_1');
$table->integer('key5_2');
$table->integer('key2_1');
$table->integer('key2_2');
$table->integer('key2_3');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('country_keypress_reports');
}
};

View File

@@ -0,0 +1,36 @@
<?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
{
Schema::create('country_voice_rating_reports', function (Blueprint $table) {
$table->id();
$table->foreignId('province_id')->constrained('provinces');
$table->string('province_name');
$table->date('date');
$table->integer('voice_quality_ok');
$table->integer('message_quality_ok');
$table->integer('voice_format_ok');
$table->integer('voice_quality_nok');
$table->integer('message_quality_nok');
$table->integer('voice_format_nok');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('country_voice_rating_reports');
}
};

View File

@@ -0,0 +1,36 @@
<?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
{
Schema::create('country_message_rating_reports', function (Blueprint $table) {
$table->id();
$table->foreignId('province_id')->constrained('provinces');
$table->string('province_name');
$table->date('date');
$table->integer('very_good');
$table->integer('good');
$table->integer('average');
$table->integer('bad');
$table->integer('very_bad');
$table->integer('wrong_code');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('country_message_rating_reports');
}
};

View File

@@ -0,0 +1,29 @@
<?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
{
Schema::table('provinces', function (Blueprint $table) {
$table->string('ip')->nullable();
$table->string('camera_ip')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('provinces', function (Blueprint $table) {
$table->dropColumn(['ip', 'camera_ip']);
});
}
};

View File

@@ -0,0 +1,27 @@
<?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
{
Schema::create('expert_performances', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('expert_performances');
}
};

View File

@@ -0,0 +1,18 @@
<?php
namespace Database\Seeders;
use App\Models\CountryCallsReport;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class CountryCallsReportSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
CountryCallsReport::factory()->count(10)->create();
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Database\Seeders;
use App\Models\CountryKeypressReport;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class CountryKeypressReportSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
CountryKeypressReport::factory()->count(10)->create();
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Database\Seeders;
use App\Models\CountryMessageRatingReport;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class CountryMessageRatingReportSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
CountryMessageRatingReport::factory()->count(5)->create();
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Database\Seeders;
use App\Models\CountryVoiceRatingReport;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class CountryVoiceRatingReportSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
CountryVoiceRatingReport::factory()->count(10)->create();
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class ExpertPerformanceSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@@ -8,6 +8,9 @@ use App\Http\Controllers\CallController;
use App\Http\Controllers\CategoryController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\ProvinceController;
use App\Http\Controllers\Setad\CumulativeCentersStatsController;
use App\Http\Controllers\Setad\CumulativeKeypressStatsController;
use App\Http\Controllers\Setad\CumulativeVoiceRatingStatsController;
use App\Http\Middleware\SuperAdminCheck;
use Illuminate\Support\Facades\Route;
@@ -92,4 +95,28 @@ Route::prefix('server')->group(function () {
Route::post('/{role}', 'update')->name('update');
Route::delete('/{role}', 'destroy')->name('destroy');
});
Route::middleware('auth')
->prefix('cumulative_stats')
->name('cumulativeStats.')
->controller(CumulativeCentersStatsController::class)
->group(function () {
Route::get('/', 'index')->name('index');
});
Route::middleware('auth')
->prefix('cumulative_voice_ratings')
->name('cumulativeVoiceRatings.')
->controller(CumulativeVoiceRatingStatsController::class)
->group(function () {
Route::get('/', 'index')->name('index');
});
Route::middleware('auth')
->prefix('cumulative_keypress_stats')
->name('cumulativeKeypressStats.')
->controller(CumulativeKeypressStatsController::class)
->group(function () {
Route::get('/', 'index')->name('index');
});
});