- SalesTenantManagement, SalesScenarioChecklist에 tenant_prospect_id 지원 추가
- 가망고객 기반 시나리오 컨트롤러 메서드 추가
- 라우트 추가: /sales/scenarios/prospect/{id}/sales, manager
- 대시보드에서 가망고객 행에 영업/매니저 버튼 및 진행률 표시
- 시나리오 모달/스텝 뷰 prospect 모드 지원
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
384 lines
11 KiB
PHP
384 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Models\Sales;
|
|
|
|
use App\Models\Tenants\Tenant;
|
|
use App\Models\User;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
|
|
/**
|
|
* 영업 시나리오 체크리스트 모델
|
|
*
|
|
* @property int $id
|
|
* @property int $tenant_id
|
|
* @property string $scenario_type (sales/manager)
|
|
* @property int $step_id
|
|
* @property string|null $checkpoint_id
|
|
* @property int|null $checkpoint_index
|
|
* @property bool $is_checked
|
|
* @property \Carbon\Carbon|null $checked_at
|
|
* @property int|null $checked_by
|
|
* @property string|null $memo
|
|
*/
|
|
class SalesScenarioChecklist extends Model
|
|
{
|
|
protected $table = 'sales_scenario_checklists';
|
|
|
|
protected $fillable = [
|
|
'tenant_id',
|
|
'tenant_prospect_id',
|
|
'scenario_type',
|
|
'step_id',
|
|
'checkpoint_id',
|
|
'checkpoint_index',
|
|
'is_checked',
|
|
'checked_at',
|
|
'checked_by',
|
|
'memo',
|
|
'user_id', // 하위 호환성
|
|
];
|
|
|
|
protected $casts = [
|
|
'step_id' => 'integer',
|
|
'checkpoint_index' => 'integer',
|
|
'is_checked' => 'boolean',
|
|
'checked_at' => 'datetime',
|
|
];
|
|
|
|
/**
|
|
* 테넌트 관계
|
|
*/
|
|
public function tenant(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Tenant::class);
|
|
}
|
|
|
|
/**
|
|
* 가망고객 관계
|
|
*/
|
|
public function tenantProspect(): BelongsTo
|
|
{
|
|
return $this->belongsTo(TenantProspect::class, 'tenant_prospect_id');
|
|
}
|
|
|
|
/**
|
|
* 체크한 사용자 관계
|
|
*/
|
|
public function checkedByUser(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'checked_by');
|
|
}
|
|
|
|
/**
|
|
* 사용자 관계 (하위 호환성)
|
|
*/
|
|
public function user(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'user_id');
|
|
}
|
|
|
|
/**
|
|
* 체크포인트 토글 (tenant_id 기반)
|
|
*/
|
|
public static function toggle(int $tenantId, string $scenarioType, int $stepId, string $checkpointId, bool $checked, ?int $userId = null): self
|
|
{
|
|
$currentUserId = $userId ?? auth()->id();
|
|
|
|
$checklist = self::firstOrNew([
|
|
'tenant_id' => $tenantId,
|
|
'scenario_type' => $scenarioType,
|
|
'step_id' => $stepId,
|
|
'checkpoint_id' => $checkpointId,
|
|
]);
|
|
|
|
// 새 레코드인 경우 필수 필드 설정
|
|
if (!$checklist->exists) {
|
|
$checklist->user_id = $currentUserId;
|
|
$checklist->checkpoint_index = 0; // 기본값
|
|
}
|
|
|
|
$checklist->is_checked = $checked;
|
|
$checklist->checked_at = $checked ? now() : null;
|
|
$checklist->checked_by = $checked ? $currentUserId : null;
|
|
$checklist->save();
|
|
|
|
return $checklist;
|
|
}
|
|
|
|
/**
|
|
* 체크포인트 토글 (prospect_id 기반)
|
|
*/
|
|
public static function toggleByProspect(int $prospectId, string $scenarioType, int $stepId, string $checkpointId, bool $checked, ?int $userId = null): self
|
|
{
|
|
$currentUserId = $userId ?? auth()->id();
|
|
|
|
$checklist = self::firstOrNew([
|
|
'tenant_prospect_id' => $prospectId,
|
|
'scenario_type' => $scenarioType,
|
|
'step_id' => $stepId,
|
|
'checkpoint_id' => $checkpointId,
|
|
]);
|
|
|
|
// 새 레코드인 경우 필수 필드 설정
|
|
if (!$checklist->exists) {
|
|
$checklist->user_id = $currentUserId;
|
|
$checklist->checkpoint_index = 0;
|
|
}
|
|
|
|
$checklist->is_checked = $checked;
|
|
$checklist->checked_at = $checked ? now() : null;
|
|
$checklist->checked_by = $checked ? $currentUserId : null;
|
|
$checklist->save();
|
|
|
|
return $checklist;
|
|
}
|
|
|
|
/**
|
|
* 특정 테넌트/시나리오의 체크리스트 조회
|
|
*/
|
|
public static function getChecklist(int $tenantId, string $scenarioType): array
|
|
{
|
|
$items = self::where('tenant_id', $tenantId)
|
|
->where('scenario_type', $scenarioType)
|
|
->where('is_checked', true)
|
|
->get();
|
|
|
|
$result = [];
|
|
foreach ($items as $item) {
|
|
$key = "{$item->step_id}_{$item->checkpoint_id}";
|
|
$result[$key] = [
|
|
'checked_at' => $item->checked_at?->toDateTimeString(),
|
|
'checked_by' => $item->checked_by,
|
|
];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 특정 가망고객/시나리오의 체크리스트 조회
|
|
*/
|
|
public static function getChecklistByProspect(int $prospectId, string $scenarioType): array
|
|
{
|
|
$items = self::where('tenant_prospect_id', $prospectId)
|
|
->where('scenario_type', $scenarioType)
|
|
->where('is_checked', true)
|
|
->get();
|
|
|
|
$result = [];
|
|
foreach ($items as $item) {
|
|
$key = "{$item->step_id}_{$item->checkpoint_id}";
|
|
$result[$key] = [
|
|
'checked_at' => $item->checked_at?->toDateTimeString(),
|
|
'checked_by' => $item->checked_by,
|
|
];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 진행률 계산
|
|
*/
|
|
public static function calculateProgress(int $tenantId, string $scenarioType, array $steps): array
|
|
{
|
|
$checklist = self::getChecklist($tenantId, $scenarioType);
|
|
|
|
$totalCheckpoints = 0;
|
|
$completedCheckpoints = 0;
|
|
$stepProgress = [];
|
|
|
|
foreach ($steps as $step) {
|
|
$stepCompleted = 0;
|
|
$stepTotal = count($step['checkpoints'] ?? []);
|
|
$totalCheckpoints += $stepTotal;
|
|
|
|
foreach ($step['checkpoints'] as $checkpoint) {
|
|
$key = "{$step['id']}_{$checkpoint['id']}";
|
|
if (isset($checklist[$key])) {
|
|
$completedCheckpoints++;
|
|
$stepCompleted++;
|
|
}
|
|
}
|
|
|
|
$stepProgress[$step['id']] = [
|
|
'total' => $stepTotal,
|
|
'completed' => $stepCompleted,
|
|
'percentage' => $stepTotal > 0 ? round(($stepCompleted / $stepTotal) * 100) : 0,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'total' => $totalCheckpoints,
|
|
'completed' => $completedCheckpoints,
|
|
'percentage' => $totalCheckpoints > 0 ? round(($completedCheckpoints / $totalCheckpoints) * 100) : 0,
|
|
'steps' => $stepProgress,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 가망고객 기반 진행률 계산
|
|
*/
|
|
public static function calculateProgressByProspect(int $prospectId, string $scenarioType, array $steps): array
|
|
{
|
|
$checklist = self::getChecklistByProspect($prospectId, $scenarioType);
|
|
|
|
$totalCheckpoints = 0;
|
|
$completedCheckpoints = 0;
|
|
$stepProgress = [];
|
|
|
|
foreach ($steps as $step) {
|
|
$stepCompleted = 0;
|
|
$stepTotal = count($step['checkpoints'] ?? []);
|
|
$totalCheckpoints += $stepTotal;
|
|
|
|
foreach ($step['checkpoints'] as $checkpoint) {
|
|
$key = "{$step['id']}_{$checkpoint['id']}";
|
|
if (isset($checklist[$key])) {
|
|
$completedCheckpoints++;
|
|
$stepCompleted++;
|
|
}
|
|
}
|
|
|
|
$stepProgress[$step['id']] = [
|
|
'total' => $stepTotal,
|
|
'completed' => $stepCompleted,
|
|
'percentage' => $stepTotal > 0 ? round(($stepCompleted / $stepTotal) * 100) : 0,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'total' => $totalCheckpoints,
|
|
'completed' => $completedCheckpoints,
|
|
'percentage' => $totalCheckpoints > 0 ? round(($completedCheckpoints / $totalCheckpoints) * 100) : 0,
|
|
'steps' => $stepProgress,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 시나리오 타입 스코프
|
|
*/
|
|
public function scopeByScenarioType($query, string $type)
|
|
{
|
|
return $query->where('scenario_type', $type);
|
|
}
|
|
|
|
/**
|
|
* 체크된 항목만 스코프
|
|
*/
|
|
public function scopeChecked($query)
|
|
{
|
|
return $query->where('is_checked', true);
|
|
}
|
|
|
|
/**
|
|
* 간단한 진행률 계산 (전체 체크포인트 수 기준)
|
|
*
|
|
* @param int $tenantId
|
|
* @param string $scenarioType 'sales' 또는 'manager'
|
|
* @return array ['completed' => 완료 수, 'total' => 전체 수, 'percentage' => 백분율]
|
|
*/
|
|
public static function getSimpleProgress(int $tenantId, string $scenarioType): array
|
|
{
|
|
// 전체 체크포인트 수 (config에서 계산)
|
|
$configKey = $scenarioType === 'sales' ? 'sales_scenario.sales_steps' : 'sales_scenario.manager_steps';
|
|
$steps = config($configKey, []);
|
|
|
|
$total = 0;
|
|
$validCheckpointKeys = [];
|
|
|
|
// config에 정의된 유효한 체크포인트만 수집
|
|
foreach ($steps as $step) {
|
|
foreach ($step['checkpoints'] ?? [] as $checkpoint) {
|
|
$total++;
|
|
$validCheckpointKeys[] = "{$step['id']}_{$checkpoint['id']}";
|
|
}
|
|
}
|
|
|
|
// 완료된 체크포인트 수 (config에 존재하는 것만 카운트)
|
|
$checkedItems = self::where('tenant_id', $tenantId)
|
|
->where('scenario_type', $scenarioType)
|
|
->where('is_checked', true)
|
|
->get();
|
|
|
|
$completed = 0;
|
|
foreach ($checkedItems as $item) {
|
|
$key = "{$item->step_id}_{$item->checkpoint_id}";
|
|
if (in_array($key, $validCheckpointKeys)) {
|
|
$completed++;
|
|
}
|
|
}
|
|
|
|
$percentage = $total > 0 ? round(($completed / $total) * 100) : 0;
|
|
|
|
return [
|
|
'completed' => $completed,
|
|
'total' => $total,
|
|
'percentage' => $percentage,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 테넌트별 영업/매니저 진행률 한번에 조회
|
|
*/
|
|
public static function getTenantProgress(int $tenantId): array
|
|
{
|
|
return [
|
|
'sales' => self::getSimpleProgress($tenantId, 'sales'),
|
|
'manager' => self::getSimpleProgress($tenantId, 'manager'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 가망고객별 간단한 진행률 계산
|
|
*/
|
|
public static function getSimpleProgressByProspect(int $prospectId, string $scenarioType): array
|
|
{
|
|
$configKey = $scenarioType === 'sales' ? 'sales_scenario.sales_steps' : 'sales_scenario.manager_steps';
|
|
$steps = config($configKey, []);
|
|
|
|
$total = 0;
|
|
$validCheckpointKeys = [];
|
|
|
|
foreach ($steps as $step) {
|
|
foreach ($step['checkpoints'] ?? [] as $checkpoint) {
|
|
$total++;
|
|
$validCheckpointKeys[] = "{$step['id']}_{$checkpoint['id']}";
|
|
}
|
|
}
|
|
|
|
$checkedItems = self::where('tenant_prospect_id', $prospectId)
|
|
->where('scenario_type', $scenarioType)
|
|
->where('is_checked', true)
|
|
->get();
|
|
|
|
$completed = 0;
|
|
foreach ($checkedItems as $item) {
|
|
$key = "{$item->step_id}_{$item->checkpoint_id}";
|
|
if (in_array($key, $validCheckpointKeys)) {
|
|
$completed++;
|
|
}
|
|
}
|
|
|
|
$percentage = $total > 0 ? round(($completed / $total) * 100) : 0;
|
|
|
|
return [
|
|
'completed' => $completed,
|
|
'total' => $total,
|
|
'percentage' => $percentage,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 가망고객별 영업/매니저 진행률 한번에 조회
|
|
*/
|
|
public static function getProspectProgress(int $prospectId): array
|
|
{
|
|
return [
|
|
'sales' => self::getSimpleProgressByProspect($prospectId, 'sales'),
|
|
'manager' => self::getSimpleProgressByProspect($prospectId, 'manager'),
|
|
];
|
|
}
|
|
}
|