feat:영업권(명함등록) 시스템 구현
- TenantProspect 모델, 서비스, 컨트롤러 추가 - 명함 등록 시 2개월 영업권 부여 - 만료 후 1개월 쿨다운 기간 적용 - 테넌트 전환 기능 구현 - 사업자번호 중복 체크 API 추가 - 명함 이미지 업로드 지원 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
212
app/Http/Controllers/Sales/TenantProspectController.php
Normal file
212
app/Http/Controllers/Sales/TenantProspectController.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Sales;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Sales\TenantProspect;
|
||||
use App\Services\Sales\TenantProspectService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* 영업권(명함등록) 관리 컨트롤러
|
||||
*/
|
||||
class TenantProspectController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private TenantProspectService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 목록 페이지
|
||||
*/
|
||||
public function index(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('sales.prospects.index'));
|
||||
}
|
||||
|
||||
$filters = [
|
||||
'search' => $request->get('search'),
|
||||
'status' => $request->get('status'),
|
||||
'registered_by' => $request->get('registered_by'),
|
||||
];
|
||||
|
||||
$prospects = $this->service->getProspects($filters)->paginate(20);
|
||||
$stats = $this->service->getStats();
|
||||
|
||||
return view('sales.prospects.index', compact('prospects', 'stats'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 등록 폼
|
||||
*/
|
||||
public function create(Request $request): View|Response
|
||||
{
|
||||
// HTMX 요청이면 JavaScript 로드를 위해 전체 페이지로 리다이렉트
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('sales.prospects.create'));
|
||||
}
|
||||
|
||||
return view('sales.prospects.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* 등록 처리
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'business_number' => 'required|string|max:20',
|
||||
'company_name' => 'required|string|max:100',
|
||||
'ceo_name' => 'nullable|string|max:50',
|
||||
'contact_phone' => 'nullable|string|max:20',
|
||||
'contact_email' => 'nullable|email|max:100',
|
||||
'address' => 'nullable|string|max:500',
|
||||
'business_card' => 'nullable|image|max:5120',
|
||||
'memo' => 'nullable|string|max:1000',
|
||||
]);
|
||||
|
||||
// 등록 가능 여부 확인
|
||||
$checkResult = $this->service->canRegister($validated['business_number']);
|
||||
if (!$checkResult['can_register']) {
|
||||
return redirect()->back()
|
||||
->withInput()
|
||||
->with('error', $checkResult['reason']);
|
||||
}
|
||||
|
||||
// 등록자는 현재 로그인 사용자
|
||||
$validated['registered_by'] = auth()->id();
|
||||
|
||||
$this->service->register(
|
||||
$validated,
|
||||
$request->file('business_card')
|
||||
);
|
||||
|
||||
return redirect()->route('sales.prospects.index')
|
||||
->with('success', '명함이 등록되었습니다. 2개월간 영업권이 유효합니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 상세 페이지
|
||||
*/
|
||||
public function show(int $id): View
|
||||
{
|
||||
$prospect = TenantProspect::with(['registeredBy', 'tenant', 'convertedBy'])
|
||||
->findOrFail($id);
|
||||
|
||||
return view('sales.prospects.show', compact('prospect'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정 폼
|
||||
*/
|
||||
public function edit(int $id): View
|
||||
{
|
||||
$prospect = TenantProspect::findOrFail($id);
|
||||
|
||||
// 이미 전환된 경우 수정 불가
|
||||
if ($prospect->isConverted()) {
|
||||
return redirect()->route('sales.prospects.show', $id)
|
||||
->with('error', '이미 테넌트로 전환된 영업권은 수정할 수 없습니다.');
|
||||
}
|
||||
|
||||
return view('sales.prospects.edit', compact('prospect'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정 처리
|
||||
*/
|
||||
public function update(Request $request, int $id)
|
||||
{
|
||||
$prospect = TenantProspect::findOrFail($id);
|
||||
|
||||
// 이미 전환된 경우 수정 불가
|
||||
if ($prospect->isConverted()) {
|
||||
return redirect()->route('sales.prospects.show', $id)
|
||||
->with('error', '이미 테넌트로 전환된 영업권은 수정할 수 없습니다.');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'company_name' => 'required|string|max:100',
|
||||
'ceo_name' => 'nullable|string|max:50',
|
||||
'contact_phone' => 'nullable|string|max:20',
|
||||
'contact_email' => 'nullable|email|max:100',
|
||||
'address' => 'nullable|string|max:500',
|
||||
'business_card' => 'nullable|image|max:5120',
|
||||
'memo' => 'nullable|string|max:1000',
|
||||
]);
|
||||
|
||||
$this->service->update(
|
||||
$prospect,
|
||||
$validated,
|
||||
$request->file('business_card')
|
||||
);
|
||||
|
||||
return redirect()->route('sales.prospects.show', $id)
|
||||
->with('success', '영업권 정보가 수정되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제 처리
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$prospect = TenantProspect::findOrFail($id);
|
||||
|
||||
// 이미 전환된 경우 삭제 불가
|
||||
if ($prospect->isConverted()) {
|
||||
return redirect()->route('sales.prospects.index')
|
||||
->with('error', '이미 테넌트로 전환된 영업권은 삭제할 수 없습니다.');
|
||||
}
|
||||
|
||||
// 본인 또는 관리자만 삭제 가능
|
||||
if ($prospect->registered_by !== auth()->id()) {
|
||||
// TODO: 관리자 권한 체크 추가
|
||||
}
|
||||
|
||||
$prospect->delete();
|
||||
|
||||
return redirect()->route('sales.prospects.index')
|
||||
->with('success', '영업권이 삭제되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 전환
|
||||
*/
|
||||
public function convert(int $id)
|
||||
{
|
||||
$prospect = TenantProspect::findOrFail($id);
|
||||
|
||||
// 이미 전환된 경우
|
||||
if ($prospect->isConverted()) {
|
||||
return redirect()->route('sales.prospects.show', $id)
|
||||
->with('error', '이미 테넌트로 전환되었습니다.');
|
||||
}
|
||||
|
||||
// 만료된 경우
|
||||
if ($prospect->isExpired()) {
|
||||
return redirect()->route('sales.prospects.show', $id)
|
||||
->with('error', '만료된 영업권은 전환할 수 없습니다.');
|
||||
}
|
||||
|
||||
$tenant = $this->service->convertToTenant($prospect, auth()->id());
|
||||
|
||||
return redirect()->route('sales.prospects.show', $id)
|
||||
->with('success', "테넌트로 전환되었습니다. (테넌트 ID: {$tenant->id})");
|
||||
}
|
||||
|
||||
/**
|
||||
* 사업자번호 중복 체크 (AJAX)
|
||||
*/
|
||||
public function checkBusinessNumber(Request $request)
|
||||
{
|
||||
$businessNumber = $request->get('business_number');
|
||||
$excludeId = $request->get('exclude_id');
|
||||
|
||||
$result = $this->service->canRegister($businessNumber, $excludeId);
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
}
|
||||
225
app/Models/Sales/TenantProspect.php
Normal file
225
app/Models/Sales/TenantProspect.php
Normal file
@@ -0,0 +1,225 @@
|
||||
<?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;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* 영업파트너 영업권(명함등록) 모델
|
||||
*/
|
||||
class TenantProspect extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'tenant_prospects';
|
||||
|
||||
public const STATUS_ACTIVE = 'active'; // 영업권 유효
|
||||
public const STATUS_EXPIRED = 'expired'; // 영업권 만료
|
||||
public const STATUS_CONVERTED = 'converted'; // 테넌트 전환 완료
|
||||
|
||||
public const VALIDITY_MONTHS = 2; // 영업권 유효기간 (개월)
|
||||
public const COOLDOWN_MONTHS = 1; // 쿨다운 기간 (개월)
|
||||
|
||||
protected $fillable = [
|
||||
'business_number',
|
||||
'company_name',
|
||||
'ceo_name',
|
||||
'contact_phone',
|
||||
'contact_email',
|
||||
'address',
|
||||
'registered_by',
|
||||
'business_card_path',
|
||||
'status',
|
||||
'registered_at',
|
||||
'expires_at',
|
||||
'cooldown_ends_at',
|
||||
'tenant_id',
|
||||
'converted_at',
|
||||
'converted_by',
|
||||
'memo',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'registered_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
'cooldown_ends_at' => 'datetime',
|
||||
'converted_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 등록한 영업파트너
|
||||
*/
|
||||
public function registeredBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'registered_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 전환된 테넌트
|
||||
*/
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class, 'tenant_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 전환 처리자
|
||||
*/
|
||||
public function convertedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'converted_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 영업권 유효 여부
|
||||
*/
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_ACTIVE && now()->lt($this->expires_at);
|
||||
}
|
||||
|
||||
/**
|
||||
* 영업권 만료 여부
|
||||
*/
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_EXPIRED || now()->gte($this->expires_at);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 전환 완료 여부
|
||||
*/
|
||||
public function isConverted(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_CONVERTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿨다운 중 여부
|
||||
*/
|
||||
public function isInCooldown(): bool
|
||||
{
|
||||
return $this->isExpired() && now()->lt($this->cooldown_ends_at);
|
||||
}
|
||||
|
||||
/**
|
||||
* 재등록 가능 여부
|
||||
*/
|
||||
public function canReRegister(): bool
|
||||
{
|
||||
return $this->isExpired() && now()->gte($this->cooldown_ends_at);
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 라벨
|
||||
*/
|
||||
public function getStatusLabelAttribute(): string
|
||||
{
|
||||
if ($this->isConverted()) {
|
||||
return '계약완료';
|
||||
}
|
||||
|
||||
if ($this->isActive()) {
|
||||
return '영업중';
|
||||
}
|
||||
|
||||
if ($this->isInCooldown()) {
|
||||
return '쿨다운';
|
||||
}
|
||||
|
||||
return '만료';
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 색상 (Tailwind CSS)
|
||||
*/
|
||||
public function getStatusColorAttribute(): string
|
||||
{
|
||||
if ($this->isConverted()) {
|
||||
return 'bg-green-100 text-green-800';
|
||||
}
|
||||
|
||||
if ($this->isActive()) {
|
||||
return 'bg-blue-100 text-blue-800';
|
||||
}
|
||||
|
||||
if ($this->isInCooldown()) {
|
||||
return 'bg-yellow-100 text-yellow-800';
|
||||
}
|
||||
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
|
||||
/**
|
||||
* 남은 일수
|
||||
*/
|
||||
public function getRemainingDaysAttribute(): int
|
||||
{
|
||||
if (!$this->isActive()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return max(0, now()->diffInDays($this->expires_at, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* 명함 이미지 URL
|
||||
*/
|
||||
public function getBusinessCardUrlAttribute(): ?string
|
||||
{
|
||||
if (!$this->business_card_path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Storage::disk('tenant')->url($this->business_card_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 명함 이미지 존재 여부
|
||||
*/
|
||||
public function hasBusinessCard(): bool
|
||||
{
|
||||
return $this->business_card_path && Storage::disk('tenant')->exists($this->business_card_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 스코프: 유효한 영업권
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_ACTIVE)
|
||||
->where('expires_at', '>', now());
|
||||
}
|
||||
|
||||
/**
|
||||
* 스코프: 만료된 영업권
|
||||
*/
|
||||
public function scopeExpired($query)
|
||||
{
|
||||
return $query->where(function ($q) {
|
||||
$q->where('status', self::STATUS_EXPIRED)
|
||||
->orWhere('expires_at', '<=', now());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 스코프: 전환 완료
|
||||
*/
|
||||
public function scopeConverted($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_CONVERTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 스코프: 특정 영업파트너의 영업권
|
||||
*/
|
||||
public function scopeByPartner($query, int $userId)
|
||||
{
|
||||
return $query->where('registered_by', $userId);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,14 @@ class Tenant extends Model
|
||||
'admin_memo',
|
||||
// 삭제 정보
|
||||
'deleted_by',
|
||||
// 영업파트너 영업권 관련
|
||||
'registered_by',
|
||||
'business_card_path',
|
||||
'prospect_status',
|
||||
'prospect_registered_at',
|
||||
'prospect_expires_at',
|
||||
'cooldown_ends_at',
|
||||
'converted_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
||||
253
app/Services/Sales/TenantProspectService.php
Normal file
253
app/Services/Sales/TenantProspectService.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Sales;
|
||||
|
||||
use App\Models\Sales\TenantProspect;
|
||||
use App\Models\Tenants\Tenant;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class TenantProspectService
|
||||
{
|
||||
/**
|
||||
* 명함 등록 (영업권 확보)
|
||||
*/
|
||||
public function register(array $data, ?UploadedFile $businessCard = null): TenantProspect
|
||||
{
|
||||
return DB::transaction(function () use ($data, $businessCard) {
|
||||
$now = now();
|
||||
$expiresAt = $now->copy()->addMonths(TenantProspect::VALIDITY_MONTHS);
|
||||
$cooldownEndsAt = $expiresAt->copy()->addMonths(TenantProspect::COOLDOWN_MONTHS);
|
||||
|
||||
// 명함 이미지 저장
|
||||
$businessCardPath = null;
|
||||
if ($businessCard) {
|
||||
$businessCardPath = $this->uploadBusinessCard($businessCard, $data['registered_by']);
|
||||
}
|
||||
|
||||
return TenantProspect::create([
|
||||
'business_number' => $data['business_number'],
|
||||
'company_name' => $data['company_name'],
|
||||
'ceo_name' => $data['ceo_name'] ?? null,
|
||||
'contact_phone' => $data['contact_phone'] ?? null,
|
||||
'contact_email' => $data['contact_email'] ?? null,
|
||||
'address' => $data['address'] ?? null,
|
||||
'registered_by' => $data['registered_by'],
|
||||
'business_card_path' => $businessCardPath,
|
||||
'status' => TenantProspect::STATUS_ACTIVE,
|
||||
'registered_at' => $now,
|
||||
'expires_at' => $expiresAt,
|
||||
'cooldown_ends_at' => $cooldownEndsAt,
|
||||
'memo' => $data['memo'] ?? null,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 영업권 정보 수정
|
||||
*/
|
||||
public function update(TenantProspect $prospect, array $data, ?UploadedFile $businessCard = null): TenantProspect
|
||||
{
|
||||
return DB::transaction(function () use ($prospect, $data, $businessCard) {
|
||||
$updateData = [
|
||||
'company_name' => $data['company_name'],
|
||||
'ceo_name' => $data['ceo_name'] ?? null,
|
||||
'contact_phone' => $data['contact_phone'] ?? null,
|
||||
'contact_email' => $data['contact_email'] ?? null,
|
||||
'address' => $data['address'] ?? null,
|
||||
'memo' => $data['memo'] ?? null,
|
||||
];
|
||||
|
||||
// 명함 이미지 교체
|
||||
if ($businessCard) {
|
||||
// 기존 이미지 삭제
|
||||
if ($prospect->business_card_path) {
|
||||
Storage::disk('tenant')->delete($prospect->business_card_path);
|
||||
}
|
||||
$updateData['business_card_path'] = $this->uploadBusinessCard($businessCard, $prospect->registered_by);
|
||||
}
|
||||
|
||||
$prospect->update($updateData);
|
||||
|
||||
return $prospect->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트로 전환
|
||||
*/
|
||||
public function convertToTenant(TenantProspect $prospect, int $convertedBy): Tenant
|
||||
{
|
||||
return DB::transaction(function () use ($prospect, $convertedBy) {
|
||||
// 테넌트 생성
|
||||
$tenant = Tenant::create([
|
||||
'company_name' => $prospect->company_name,
|
||||
'business_num' => $prospect->business_number,
|
||||
'ceo_name' => $prospect->ceo_name,
|
||||
'phone' => $prospect->contact_phone,
|
||||
'email' => $prospect->contact_email,
|
||||
'address' => $prospect->address,
|
||||
'tenant_st_code' => 'trial',
|
||||
'tenant_type' => 'customer',
|
||||
'created_by' => $convertedBy,
|
||||
]);
|
||||
|
||||
// 영업권 상태 업데이트
|
||||
$prospect->update([
|
||||
'status' => TenantProspect::STATUS_CONVERTED,
|
||||
'tenant_id' => $tenant->id,
|
||||
'converted_at' => now(),
|
||||
'converted_by' => $convertedBy,
|
||||
]);
|
||||
|
||||
return $tenant;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 영업권 만료 처리 (배치용)
|
||||
*/
|
||||
public function expireOldProspects(): int
|
||||
{
|
||||
return TenantProspect::where('status', TenantProspect::STATUS_ACTIVE)
|
||||
->where('expires_at', '<=', now())
|
||||
->update(['status' => TenantProspect::STATUS_EXPIRED]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사업자번호로 등록 가능 여부 확인
|
||||
*/
|
||||
public function canRegister(string $businessNumber, ?int $excludeId = null): array
|
||||
{
|
||||
$query = TenantProspect::where('business_number', $businessNumber);
|
||||
|
||||
if ($excludeId) {
|
||||
$query->where('id', '!=', $excludeId);
|
||||
}
|
||||
|
||||
// 이미 전환된 경우
|
||||
$converted = (clone $query)->where('status', TenantProspect::STATUS_CONVERTED)->first();
|
||||
if ($converted) {
|
||||
return [
|
||||
'can_register' => false,
|
||||
'reason' => '이미 테넌트로 전환된 회사입니다.',
|
||||
'prospect' => $converted,
|
||||
];
|
||||
}
|
||||
|
||||
// 유효한 영업권이 있는 경우
|
||||
$active = (clone $query)->active()->first();
|
||||
if ($active) {
|
||||
return [
|
||||
'can_register' => false,
|
||||
'reason' => "이미 {$active->registeredBy->name}님이 영업권을 보유 중입니다. (만료: {$active->expires_at->format('Y-m-d')})",
|
||||
'prospect' => $active,
|
||||
];
|
||||
}
|
||||
|
||||
// 쿨다운 중인 경우
|
||||
$inCooldown = (clone $query)
|
||||
->where('status', TenantProspect::STATUS_EXPIRED)
|
||||
->where('cooldown_ends_at', '>', now())
|
||||
->first();
|
||||
|
||||
if ($inCooldown) {
|
||||
return [
|
||||
'can_register' => false,
|
||||
'reason' => "쿨다운 기간 중입니다. (등록 가능: {$inCooldown->cooldown_ends_at->format('Y-m-d')})",
|
||||
'prospect' => $inCooldown,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'can_register' => true,
|
||||
'reason' => null,
|
||||
'prospect' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 목록 조회
|
||||
*/
|
||||
public function getProspects(array $filters = [])
|
||||
{
|
||||
$query = TenantProspect::with(['registeredBy', 'tenant']);
|
||||
|
||||
// 검색
|
||||
if (!empty($filters['search'])) {
|
||||
$search = $filters['search'];
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('company_name', 'like', "%{$search}%")
|
||||
->orWhere('business_number', 'like', "%{$search}%")
|
||||
->orWhere('ceo_name', 'like', "%{$search}%")
|
||||
->orWhere('contact_phone', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (!empty($filters['status'])) {
|
||||
if ($filters['status'] === 'active') {
|
||||
$query->active();
|
||||
} elseif ($filters['status'] === 'expired') {
|
||||
$query->where('status', TenantProspect::STATUS_EXPIRED);
|
||||
} elseif ($filters['status'] === 'converted') {
|
||||
$query->converted();
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 영업파트너
|
||||
if (!empty($filters['registered_by'])) {
|
||||
$query->byPartner($filters['registered_by']);
|
||||
}
|
||||
|
||||
return $query->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 조회
|
||||
*/
|
||||
public function getStats(?int $partnerId = null): array
|
||||
{
|
||||
$baseQuery = TenantProspect::query();
|
||||
|
||||
if ($partnerId) {
|
||||
$baseQuery->byPartner($partnerId);
|
||||
}
|
||||
|
||||
return [
|
||||
'total' => (clone $baseQuery)->count(),
|
||||
'active' => (clone $baseQuery)->active()->count(),
|
||||
'expired' => (clone $baseQuery)->where('status', TenantProspect::STATUS_EXPIRED)->count(),
|
||||
'converted' => (clone $baseQuery)->converted()->count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 명함 이미지 업로드
|
||||
*/
|
||||
private function uploadBusinessCard(UploadedFile $file, int $userId): string
|
||||
{
|
||||
$storedName = Str::uuid() . '.' . $file->getClientOriginalExtension();
|
||||
$filePath = "prospects/{$userId}/{$storedName}";
|
||||
|
||||
Storage::disk('tenant')->put($filePath, file_get_contents($file));
|
||||
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 명함 이미지 삭제
|
||||
*/
|
||||
public function deleteBusinessCard(TenantProspect $prospect): bool
|
||||
{
|
||||
if ($prospect->business_card_path && Storage::disk('tenant')->exists($prospect->business_card_path)) {
|
||||
Storage::disk('tenant')->delete($prospect->business_card_path);
|
||||
$prospect->update(['business_card_path' => null]);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '가망고객 등록')
|
||||
@section('title', '명함 등록')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto">
|
||||
@@ -12,16 +12,42 @@
|
||||
</svg>
|
||||
목록으로
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold text-gray-800">가망고객 등록</h1>
|
||||
<h1 class="text-2xl font-bold text-gray-800">명함 등록</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">명함 등록 시 2개월간 영업권이 부여됩니다</p>
|
||||
</div>
|
||||
|
||||
<!-- 알림 메시지 -->
|
||||
@if(session('error'))
|
||||
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- 폼 -->
|
||||
<form action="{{ route('sales.prospects.store') }}" method="POST" class="bg-white rounded-lg shadow-sm p-6 space-y-6">
|
||||
<form action="{{ route('sales.prospects.store') }}" method="POST" enctype="multipart/form-data" class="bg-white rounded-lg shadow-sm p-6 space-y-6">
|
||||
@csrf
|
||||
|
||||
<!-- 사업자번호 (중복 체크) -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">사업자번호 <span class="text-red-500">*</span></label>
|
||||
<div class="flex gap-2">
|
||||
<input type="text" name="business_number" id="business_number" value="{{ old('business_number') }}" required
|
||||
placeholder="000-00-00000"
|
||||
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 @error('business_number') border-red-500 @enderror">
|
||||
<button type="button" id="checkBusinessNumber"
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition">
|
||||
중복확인
|
||||
</button>
|
||||
</div>
|
||||
<p id="businessNumberResult" class="mt-1 text-sm"></p>
|
||||
@error('business_number')
|
||||
<p class="mt-1 text-sm text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">업체명 <span class="text-red-500">*</span></label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">회사명 <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="company_name" value="{{ old('company_name') }}" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 @error('company_name') border-red-500 @enderror">
|
||||
@error('company_name')
|
||||
@@ -30,43 +56,24 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">사업자번호</label>
|
||||
<input type="text" name="business_no" value="{{ old('business_no') }}" placeholder="000-00-00000"
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">대표자명</label>
|
||||
<input type="text" name="ceo_name" value="{{ old('ceo_name') }}"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">대표자명</label>
|
||||
<input type="text" name="representative" value="{{ old('representative') }}"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">연락처</label>
|
||||
<input type="text" name="contact_phone" value="{{ old('contact_phone') }}"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">이메일</label>
|
||||
<input type="email" name="email" value="{{ old('email') }}"
|
||||
<input type="email" name="contact_email" value="{{ old('contact_email') }}"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">상태 <span class="text-red-500">*</span></label>
|
||||
<select name="status" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="lead" {{ old('status') === 'lead' ? 'selected' : '' }}>리드</option>
|
||||
<option value="prospect" {{ old('status') === 'prospect' ? 'selected' : '' }}>가망</option>
|
||||
<option value="negotiation" {{ old('status') === 'negotiation' ? 'selected' : '' }}>협상중</option>
|
||||
<option value="contracted" {{ old('status') === 'contracted' ? 'selected' : '' }}>계약완료</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -75,35 +82,27 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">영업 담당자 <span class="text-red-500">*</span></label>
|
||||
<select name="manager_id" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 @error('manager_id') border-red-500 @enderror">
|
||||
<option value="">선택하세요</option>
|
||||
@foreach($managers as $manager)
|
||||
<option value="{{ $manager->id }}" {{ old('manager_id') == $manager->id ? 'selected' : '' }}>
|
||||
{{ $manager->name }} ({{ $manager->role_label }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('manager_id')
|
||||
<p class="mt-1 text-sm text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">명함 이미지</label>
|
||||
<input type="file" name="business_card" accept="image/*"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<p class="mt-1 text-xs text-gray-500">JPG, PNG 형식 (최대 5MB)</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">담당 매니저</label>
|
||||
<select name="sales_manager_id"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">선택 안함</option>
|
||||
@foreach($managers as $manager)
|
||||
<option value="{{ $manager->id }}" {{ old('sales_manager_id') == $manager->id ? 'selected' : '' }}>
|
||||
{{ $manager->name }} ({{ $manager->role_label }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">메모</label>
|
||||
<textarea name="memo" rows="3"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">{{ old('memo') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h3 class="font-medium text-blue-800 mb-2">영업권 안내</h3>
|
||||
<ul class="text-sm text-blue-700 space-y-1">
|
||||
<li>등록일로부터 <strong>2개월간</strong> 영업권이 유효합니다.</li>
|
||||
<li>유효기간 내 테넌트로 전환 시 영업 실적으로 인정됩니다.</li>
|
||||
<li>만료 후 <strong>1개월간</strong> 쿨다운 기간이 적용됩니다.</li>
|
||||
<li>쿨다운 이후 다른 영업파트너가 등록할 수 있습니다.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4 border-t">
|
||||
@@ -118,4 +117,42 @@ class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.getElementById('checkBusinessNumber').addEventListener('click', function() {
|
||||
const businessNumber = document.getElementById('business_number').value;
|
||||
const resultEl = document.getElementById('businessNumberResult');
|
||||
|
||||
if (!businessNumber) {
|
||||
resultEl.textContent = '사업자번호를 입력해주세요.';
|
||||
resultEl.className = 'mt-1 text-sm text-red-500';
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('{{ route("sales.prospects.check-business-number") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({ business_number: businessNumber })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.can_register) {
|
||||
resultEl.textContent = '등록 가능한 사업자번호입니다.';
|
||||
resultEl.className = 'mt-1 text-sm text-green-500';
|
||||
} else {
|
||||
resultEl.textContent = data.reason;
|
||||
resultEl.className = 'mt-1 text-sm text-red-500';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
resultEl.textContent = '확인 중 오류가 발생했습니다.';
|
||||
resultEl.className = 'mt-1 text-sm text-red-500';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
|
||||
@@ -1,29 +1,44 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '가망고객 수정')
|
||||
@section('title', '영업권 수정')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="mb-6">
|
||||
<a href="{{ route('sales.prospects.index') }}" class="text-gray-500 hover:text-gray-700 text-sm mb-2 inline-flex items-center">
|
||||
<a href="{{ route('sales.prospects.show', $prospect->id) }}" class="text-gray-500 hover:text-gray-700 text-sm mb-2 inline-flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
목록으로
|
||||
상세로 돌아가기
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold text-gray-800">가망고객 수정</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ $prospect->company_name }}</p>
|
||||
<h1 class="text-2xl font-bold text-gray-800">영업권 수정</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ $prospect->company_name }} ({{ $prospect->business_number }})</p>
|
||||
</div>
|
||||
|
||||
<!-- 알림 메시지 -->
|
||||
@if(session('error'))
|
||||
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- 폼 -->
|
||||
<form action="{{ route('sales.prospects.update', $prospect->id) }}" method="POST" class="bg-white rounded-lg shadow-sm p-6 space-y-6">
|
||||
<form action="{{ route('sales.prospects.update', $prospect->id) }}" method="POST" enctype="multipart/form-data" class="bg-white rounded-lg shadow-sm p-6 space-y-6">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<!-- 사업자번호 (수정 불가) -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">사업자번호</label>
|
||||
<input type="text" value="{{ $prospect->business_number }}" disabled
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-100 text-gray-500">
|
||||
<p class="mt-1 text-xs text-gray-500">사업자번호는 수정할 수 없습니다</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">업체명 <span class="text-red-500">*</span></label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">회사명 <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="company_name" value="{{ old('company_name', $prospect->company_name) }}" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 @error('company_name') border-red-500 @enderror">
|
||||
@error('company_name')
|
||||
@@ -32,44 +47,24 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">사업자번호</label>
|
||||
<input type="text" name="business_no" value="{{ old('business_no', $prospect->business_no) }}" placeholder="000-00-00000"
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">대표자명</label>
|
||||
<input type="text" name="ceo_name" value="{{ old('ceo_name', $prospect->ceo_name) }}"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">대표자명</label>
|
||||
<input type="text" name="representative" value="{{ old('representative', $prospect->representative) }}"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">연락처</label>
|
||||
<input type="text" name="contact_phone" value="{{ old('contact_phone', $prospect->contact_phone) }}"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">이메일</label>
|
||||
<input type="email" name="email" value="{{ old('email', $prospect->email) }}"
|
||||
<input type="email" name="contact_email" value="{{ old('contact_email', $prospect->contact_email) }}"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">상태 <span class="text-red-500">*</span></label>
|
||||
<select name="status" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="lead" {{ old('status', $prospect->status) === 'lead' ? 'selected' : '' }}>리드</option>
|
||||
<option value="prospect" {{ old('status', $prospect->status) === 'prospect' ? 'selected' : '' }}>가망</option>
|
||||
<option value="negotiation" {{ old('status', $prospect->status) === 'negotiation' ? 'selected' : '' }}>협상중</option>
|
||||
<option value="contracted" {{ old('status', $prospect->status) === 'contracted' ? 'selected' : '' }}>계약완료</option>
|
||||
<option value="lost" {{ old('status', $prospect->status) === 'lost' ? 'selected' : '' }}>실패</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -79,20 +74,45 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">담당 매니저</label>
|
||||
<select name="sales_manager_id"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">선택 안함</option>
|
||||
@foreach($managers as $manager)
|
||||
<option value="{{ $manager->id }}" {{ old('sales_manager_id', $prospect->sales_manager_id) == $manager->id ? 'selected' : '' }}>
|
||||
{{ $manager->name }} ({{ $manager->role_label }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">명함 이미지</label>
|
||||
@if($prospect->hasBusinessCard())
|
||||
<div class="mb-2 p-2 bg-gray-50 rounded-lg">
|
||||
<img src="{{ $prospect->business_card_url }}" alt="현재 명함" class="max-h-32 rounded">
|
||||
<p class="text-xs text-gray-500 mt-1">새 이미지를 업로드하면 기존 이미지가 교체됩니다</p>
|
||||
</div>
|
||||
@endif
|
||||
<input type="file" name="business_card" accept="image/*"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<p class="mt-1 text-xs text-gray-500">JPG, PNG 형식 (최대 5MB)</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">메모</label>
|
||||
<textarea name="memo" rows="3"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">{{ old('memo', $prospect->memo) }}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- 영업권 상태 정보 -->
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<h3 class="font-medium text-gray-800 mb-2">영업권 상태</h3>
|
||||
<dl class="grid grid-cols-2 gap-2 text-sm">
|
||||
<dt class="text-gray-500">상태</dt>
|
||||
<dd class="font-medium">
|
||||
<span class="px-2 py-1 text-xs font-medium rounded-full {{ $prospect->status_color }}">
|
||||
{{ $prospect->status_label }}
|
||||
</span>
|
||||
</dd>
|
||||
<dt class="text-gray-500">등록일</dt>
|
||||
<dd class="font-medium">{{ $prospect->registered_at->format('Y-m-d') }}</dd>
|
||||
<dt class="text-gray-500">만료일</dt>
|
||||
<dd class="font-medium">{{ $prospect->expires_at->format('Y-m-d') }}</dd>
|
||||
<dt class="text-gray-500">등록자</dt>
|
||||
<dd class="font-medium">{{ $prospect->registeredBy?->name ?? '-' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4 border-t">
|
||||
<a href="{{ route('sales.prospects.index') }}"
|
||||
<a href="{{ route('sales.prospects.show', $prospect->id) }}"
|
||||
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition">
|
||||
취소
|
||||
</a>
|
||||
|
||||
@@ -1,53 +1,41 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '가망고객 관리')
|
||||
@section('title', '명함등록 (영업권)')
|
||||
|
||||
@section('content')
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6 flex-shrink-0">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-800">가망고객 관리</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">영업 가망고객을 관리합니다</p>
|
||||
<h1 class="text-2xl font-bold text-gray-800">명함등록 (영업권)</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">명함을 등록하여 2개월간 영업권을 확보하세요</p>
|
||||
</div>
|
||||
<a href="{{ route('sales.prospects.create') }}"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition text-center w-full sm:w-auto flex items-center justify-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
가망고객 등록
|
||||
명함 등록
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 통계 카드 -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-4 mb-6 flex-shrink-0">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6 flex-shrink-0">
|
||||
<div class="bg-white rounded-lg shadow-sm p-4">
|
||||
<div class="text-sm text-gray-500">전체</div>
|
||||
<div class="text-xl font-bold text-gray-800">{{ number_format($stats['total']) }}</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-lg shadow-sm p-4">
|
||||
<div class="text-sm text-gray-600">리드</div>
|
||||
<div class="text-xl font-bold text-gray-800">{{ number_format($stats['lead']) }}</div>
|
||||
</div>
|
||||
<div class="bg-blue-50 rounded-lg shadow-sm p-4">
|
||||
<div class="text-sm text-blue-600">가망</div>
|
||||
<div class="text-xl font-bold text-blue-800">{{ number_format($stats['prospect']) }}</div>
|
||||
<div class="text-sm text-blue-600">영업중</div>
|
||||
<div class="text-xl font-bold text-blue-800">{{ number_format($stats['active']) }}</div>
|
||||
</div>
|
||||
<div class="bg-yellow-50 rounded-lg shadow-sm p-4">
|
||||
<div class="text-sm text-yellow-600">협상중</div>
|
||||
<div class="text-xl font-bold text-yellow-800">{{ number_format($stats['negotiation']) }}</div>
|
||||
<div class="bg-gray-50 rounded-lg shadow-sm p-4">
|
||||
<div class="text-sm text-gray-600">만료</div>
|
||||
<div class="text-xl font-bold text-gray-800">{{ number_format($stats['expired']) }}</div>
|
||||
</div>
|
||||
<div class="bg-green-50 rounded-lg shadow-sm p-4">
|
||||
<div class="text-sm text-green-600">계약완료</div>
|
||||
<div class="text-xl font-bold text-green-800">{{ number_format($stats['contracted']) }}</div>
|
||||
</div>
|
||||
<div class="bg-purple-50 rounded-lg shadow-sm p-4">
|
||||
<div class="text-sm text-purple-600">총 계약금액</div>
|
||||
<div class="text-xl font-bold text-purple-800">{{ number_format($stats['total_contract']) }}</div>
|
||||
</div>
|
||||
<div class="bg-indigo-50 rounded-lg shadow-sm p-4">
|
||||
<div class="text-sm text-indigo-600">총 수수료</div>
|
||||
<div class="text-xl font-bold text-indigo-800">{{ number_format($stats['total_commission']) }}</div>
|
||||
<div class="text-xl font-bold text-green-800">{{ number_format($stats['converted']) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,27 +46,15 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition
|
||||
<input type="text"
|
||||
name="search"
|
||||
value="{{ request('search') }}"
|
||||
placeholder="업체명, 사업자번호, 대표자로 검색..."
|
||||
placeholder="회사명, 사업자번호, 대표자, 연락처로 검색..."
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div class="w-full sm:w-40">
|
||||
<select name="status" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">전체 상태</option>
|
||||
<option value="lead" {{ request('status') === 'lead' ? 'selected' : '' }}>리드</option>
|
||||
<option value="prospect" {{ request('status') === 'prospect' ? 'selected' : '' }}>가망</option>
|
||||
<option value="negotiation" {{ request('status') === 'negotiation' ? 'selected' : '' }}>협상중</option>
|
||||
<option value="contracted" {{ request('status') === 'contracted' ? 'selected' : '' }}>계약완료</option>
|
||||
<option value="lost" {{ request('status') === 'lost' ? 'selected' : '' }}>실패</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="w-full sm:w-40">
|
||||
<select name="manager_id" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">전체 담당자</option>
|
||||
@foreach($managers as $manager)
|
||||
<option value="{{ $manager->id }}" {{ request('manager_id') == $manager->id ? 'selected' : '' }}>
|
||||
{{ $manager->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
<option value="active" {{ request('status') === 'active' ? 'selected' : '' }}>영업중</option>
|
||||
<option value="expired" {{ request('status') === 'expired' ? 'selected' : '' }}>만료</option>
|
||||
<option value="converted" {{ request('status') === 'converted' ? 'selected' : '' }}>계약완료</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition w-full sm:w-auto">
|
||||
@@ -93,12 +69,11 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">업체정보</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">회사정보</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">상태</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">담당자</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">등록자</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">연락처</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">계약금액</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">등록일</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">유효기간</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -107,53 +82,61 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4">
|
||||
<div class="font-medium text-gray-900">{{ $prospect->company_name }}</div>
|
||||
@if($prospect->business_no)
|
||||
<div class="text-sm text-gray-500">{{ $prospect->formatted_business_no }}</div>
|
||||
@endif
|
||||
@if($prospect->representative)
|
||||
<div class="text-sm text-gray-500">{{ $prospect->representative }}</div>
|
||||
<div class="text-sm text-gray-500">{{ $prospect->business_number }}</div>
|
||||
@if($prospect->ceo_name)
|
||||
<div class="text-sm text-gray-500">{{ $prospect->ceo_name }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 py-1 text-xs font-medium rounded-full {{ $prospect->status_color }}">
|
||||
{{ $prospect->status_label }}
|
||||
</span>
|
||||
@if($prospect->isActive() && $prospect->remaining_days <= 14)
|
||||
<div class="text-xs text-red-500 mt-1">D-{{ $prospect->remaining_days }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">{{ $prospect->manager?->name ?? '-' }}</div>
|
||||
@if($prospect->salesManager && $prospect->salesManager->id !== $prospect->manager?->id)
|
||||
<div class="text-xs text-gray-500">담당: {{ $prospect->salesManager->name }}</div>
|
||||
@endif
|
||||
<div class="text-sm text-gray-900">{{ $prospect->registeredBy?->name ?? '-' }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $prospect->contact_phone ?? '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm">
|
||||
@if($prospect->products->count() > 0)
|
||||
<div class="font-medium text-gray-900">{{ number_format($prospect->total_contract_amount) }}원</div>
|
||||
<div class="text-xs text-gray-500">수수료: {{ number_format($prospect->total_commission) }}원</div>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
@if($prospect->isConverted())
|
||||
<div class="text-green-600">{{ $prospect->converted_at?->format('Y-m-d') }} 전환</div>
|
||||
@elseif($prospect->isActive())
|
||||
<div class="text-blue-600">{{ $prospect->expires_at->format('Y-m-d') }} 까지</div>
|
||||
@else
|
||||
<span class="text-gray-400">-</span>
|
||||
<div class="text-gray-500">{{ $prospect->expires_at->format('Y-m-d') }} 만료</div>
|
||||
@if($prospect->isInCooldown())
|
||||
<div class="text-xs text-yellow-600">쿨다운: {{ $prospect->cooldown_ends_at->format('Y-m-d') }}</div>
|
||||
@endif
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $prospect->created_at->format('Y-m-d') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<a href="{{ route('sales.prospects.show', $prospect->id) }}" class="text-blue-600 hover:text-blue-900 mr-3">상세</a>
|
||||
@if(!$prospect->isConverted())
|
||||
<a href="{{ route('sales.prospects.edit', $prospect->id) }}" class="text-indigo-600 hover:text-indigo-900 mr-3">수정</a>
|
||||
@if($prospect->isActive())
|
||||
<form action="{{ route('sales.prospects.convert', $prospect->id) }}" method="POST" class="inline"
|
||||
onsubmit="return confirm('테넌트로 전환하시겠습니까?')">
|
||||
@csrf
|
||||
<button type="submit" class="text-green-600 hover:text-green-900 mr-3">전환</button>
|
||||
</form>
|
||||
@endif
|
||||
<form action="{{ route('sales.prospects.destroy', $prospect->id) }}" method="POST" class="inline"
|
||||
onsubmit="return confirm('정말 삭제하시겠습니까?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:text-red-900">삭제</button>
|
||||
</form>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="px-6 py-12 text-center text-gray-500">
|
||||
등록된 가망고객이 없습니다.
|
||||
<td colspan="6" class="px-6 py-12 text-center text-gray-500">
|
||||
등록된 명함이 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '가망고객 상세')
|
||||
@section('title', '영업권 상세')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="mb-6 flex justify-between items-start">
|
||||
<div>
|
||||
@@ -18,25 +18,57 @@
|
||||
<span class="px-2 py-1 text-xs font-medium rounded-full {{ $prospect->status_color }}">
|
||||
{{ $prospect->status_label }}
|
||||
</span>
|
||||
@if($prospect->business_no)
|
||||
<span class="ml-2">{{ $prospect->formatted_business_no }}</span>
|
||||
@endif
|
||||
<span class="ml-2">{{ $prospect->business_number }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<a href="{{ route('sales.prospects.edit', $prospect->id) }}"
|
||||
class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transition">
|
||||
수정
|
||||
</a>
|
||||
<div class="flex gap-2">
|
||||
@if(!$prospect->isConverted())
|
||||
<a href="{{ route('sales.prospects.edit', $prospect->id) }}"
|
||||
class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transition">
|
||||
수정
|
||||
</a>
|
||||
@if($prospect->isActive())
|
||||
<form action="{{ route('sales.prospects.convert', $prospect->id) }}" method="POST"
|
||||
onsubmit="return confirm('테넌트로 전환하시겠습니까?\n\n전환 후에는 수정/삭제가 불가능합니다.')">
|
||||
@csrf
|
||||
<button type="submit"
|
||||
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition">
|
||||
테넌트 전환
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- 알림 메시지 -->
|
||||
@if(session('success'))
|
||||
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg text-green-700">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- 기본 정보 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">기본 정보</h2>
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">회사 정보</h2>
|
||||
<dl class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">사업자번호</dt>
|
||||
<dd class="font-medium text-gray-900">{{ $prospect->business_number }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">회사명</dt>
|
||||
<dd class="font-medium text-gray-900">{{ $prospect->company_name }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">대표자</dt>
|
||||
<dd class="font-medium text-gray-900">{{ $prospect->representative ?? '-' }}</dd>
|
||||
<dd class="font-medium text-gray-900">{{ $prospect->ceo_name ?? '-' }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">연락처</dt>
|
||||
@@ -44,7 +76,7 @@ class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transit
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">이메일</dt>
|
||||
<dd class="font-medium text-gray-900">{{ $prospect->email ?? '-' }}</dd>
|
||||
<dd class="font-medium text-gray-900">{{ $prospect->contact_email ?? '-' }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">주소</dt>
|
||||
@@ -53,120 +85,107 @@ class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transit
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- 담당자 정보 -->
|
||||
<!-- 영업권 정보 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">담당자 정보</h2>
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">영업권 정보</h2>
|
||||
<dl class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">영업 담당자</dt>
|
||||
<dd class="font-medium text-gray-900">
|
||||
@if($prospect->manager)
|
||||
<a href="{{ route('sales.managers.show', $prospect->manager->id) }}" class="text-blue-600 hover:underline">
|
||||
{{ $prospect->manager->name }}
|
||||
</a>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">담당 매니저</dt>
|
||||
<dd class="font-medium text-gray-900">
|
||||
@if($prospect->salesManager)
|
||||
<a href="{{ route('sales.managers.show', $prospect->salesManager->id) }}" class="text-blue-600 hover:underline">
|
||||
{{ $prospect->salesManager->name }}
|
||||
</a>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</dd>
|
||||
<dt class="text-gray-500">등록자</dt>
|
||||
<dd class="font-medium text-gray-900">{{ $prospect->registeredBy?->name ?? '-' }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">등록일</dt>
|
||||
<dd class="font-medium text-gray-900">{{ $prospect->created_at->format('Y-m-d H:i') }}</dd>
|
||||
<dd class="font-medium text-gray-900">{{ $prospect->registered_at->format('Y-m-d H:i') }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">만료일</dt>
|
||||
<dd class="font-medium {{ $prospect->isActive() ? 'text-blue-600' : 'text-gray-500' }}">
|
||||
{{ $prospect->expires_at->format('Y-m-d H:i') }}
|
||||
@if($prospect->isActive())
|
||||
<span class="text-sm">(D-{{ $prospect->remaining_days }})</span>
|
||||
@endif
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">쿨다운 종료일</dt>
|
||||
<dd class="font-medium text-gray-900">{{ $prospect->cooldown_ends_at->format('Y-m-d H:i') }}</dd>
|
||||
</div>
|
||||
@if($prospect->isConverted())
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">전환일</dt>
|
||||
<dd class="font-medium text-green-600">{{ $prospect->converted_at?->format('Y-m-d H:i') }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">전환 처리자</dt>
|
||||
<dd class="font-medium text-gray-900">{{ $prospect->convertedBy?->name ?? '-' }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">전환된 테넌트</dt>
|
||||
<dd class="font-medium text-blue-600">
|
||||
@if($prospect->tenant)
|
||||
<a href="{{ route('tenants.edit', $prospect->tenant_id) }}" class="hover:underline">
|
||||
{{ $prospect->tenant->company_name }} (ID: {{ $prospect->tenant_id }})
|
||||
</a>
|
||||
@else
|
||||
ID: {{ $prospect->tenant_id }}
|
||||
@endif
|
||||
</dd>
|
||||
</div>
|
||||
@endif
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- 통계 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">계약 현황</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="bg-blue-50 rounded-lg p-4 text-center">
|
||||
<div class="text-2xl font-bold text-blue-800">{{ number_format($prospect->total_contract_amount) }}원</div>
|
||||
<div class="text-sm text-blue-600">총 계약금액</div>
|
||||
</div>
|
||||
<div class="bg-green-50 rounded-lg p-4 text-center">
|
||||
<div class="text-2xl font-bold text-green-800">{{ number_format($prospect->total_commission) }}원</div>
|
||||
<div class="text-sm text-green-600">총 수수료</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-lg p-4 text-center">
|
||||
<div class="text-2xl font-bold text-gray-800">{{ $prospect->products->count() }}건</div>
|
||||
<div class="text-sm text-gray-600">계약 상품</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 계약 상품 목록 -->
|
||||
<!-- 명함 이미지 -->
|
||||
@if($prospect->hasBusinessCard())
|
||||
<div class="mt-6 bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">계약 상품</h2>
|
||||
@if($prospect->products->isNotEmpty())
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">상품명</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">계약금액</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">수수료</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">승인상태</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">계약일</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@foreach($prospect->products as $product)
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium text-gray-900">{{ $product->product_name }}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-900">{{ number_format($product->contract_amount) }}원</td>
|
||||
<td class="px-4 py-3 text-right text-gray-900">{{ number_format($product->commission_amount) }}원</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<span class="px-2 py-1 text-xs font-medium rounded-full {{ $product->approval_color }}">
|
||||
{{ $product->approval_status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-500">{{ $product->contract_date?->format('Y-m-d') ?? '-' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">명함 이미지</h2>
|
||||
<div class="flex justify-center">
|
||||
<img src="{{ $prospect->business_card_url }}" alt="명함 이미지" class="max-w-md rounded-lg shadow">
|
||||
</div>
|
||||
@else
|
||||
<p class="text-center text-gray-500 py-8">등록된 계약 상품이 없습니다.</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- 상담 기록 -->
|
||||
<!-- 메모 -->
|
||||
@if($prospect->memo)
|
||||
<div class="mt-6 bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">상담 기록</h2>
|
||||
@if($prospect->consultations->isNotEmpty())
|
||||
<div class="space-y-4">
|
||||
@foreach($prospect->consultations->take(10) as $consultation)
|
||||
<div class="border-l-4 border-blue-400 pl-4 py-2">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-900">{{ $consultation->manager?->name ?? '알 수 없음' }}</span>
|
||||
<span class="text-xs text-gray-500 ml-2">{{ $consultation->created_at->format('Y-m-d H:i') }}</span>
|
||||
</div>
|
||||
<span class="px-2 py-1 text-xs bg-gray-100 text-gray-600 rounded">
|
||||
{{ $consultation->consultation_type_label }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-1 text-gray-700 whitespace-pre-line">{{ $consultation->log_text }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">메모</h2>
|
||||
<p class="text-gray-700 whitespace-pre-line">{{ $prospect->memo }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- 상태별 안내 -->
|
||||
<div class="mt-6">
|
||||
@if($prospect->isActive())
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h3 class="font-medium text-blue-800 mb-2">영업권 유효</h3>
|
||||
<p class="text-sm text-blue-700">
|
||||
{{ $prospect->expires_at->format('Y-m-d') }}까지 영업권이 유효합니다.
|
||||
남은 기간: <strong>{{ $prospect->remaining_days }}일</strong>
|
||||
</p>
|
||||
</div>
|
||||
@elseif($prospect->isInCooldown())
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<h3 class="font-medium text-yellow-800 mb-2">쿨다운 기간</h3>
|
||||
<p class="text-sm text-yellow-700">
|
||||
영업권이 만료되었습니다.
|
||||
{{ $prospect->cooldown_ends_at->format('Y-m-d') }} 이후 다른 영업파트너가 재등록할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
@elseif($prospect->isConverted())
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<h3 class="font-medium text-green-800 mb-2">테넌트 전환 완료</h3>
|
||||
<p class="text-sm text-green-700">
|
||||
{{ $prospect->converted_at?->format('Y-m-d') }}에 테넌트로 전환되었습니다.
|
||||
</p>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-center text-gray-500 py-8">등록된 상담 기록이 없습니다.</p>
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<h3 class="font-medium text-gray-800 mb-2">영업권 만료</h3>
|
||||
<p class="text-sm text-gray-700">
|
||||
영업권이 만료되었습니다. 쿨다운 기간이 종료되어 재등록이 가능합니다.
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -802,8 +802,10 @@
|
||||
Route::get('managers/{id}/documents/{documentId}/download', [\App\Http\Controllers\Sales\SalesManagerController::class, 'downloadDocument'])->name('managers.documents.download');
|
||||
Route::delete('managers/{id}/documents/{documentId}', [\App\Http\Controllers\Sales\SalesManagerController::class, 'deleteDocument'])->name('managers.documents.delete');
|
||||
|
||||
// 가망고객 관리
|
||||
Route::resource('prospects', \App\Http\Controllers\Sales\SalesProspectController::class);
|
||||
// 명함등록 (영업권) 관리
|
||||
Route::resource('prospects', \App\Http\Controllers\Sales\TenantProspectController::class);
|
||||
Route::post('prospects/{id}/convert', [\App\Http\Controllers\Sales\TenantProspectController::class, 'convert'])->name('prospects.convert');
|
||||
Route::post('prospects/check-business-number', [\App\Http\Controllers\Sales\TenantProspectController::class, 'checkBusinessNumber'])->name('prospects.check-business-number');
|
||||
|
||||
// 영업 실적 관리
|
||||
Route::resource('records', \App\Http\Controllers\Sales\SalesRecordController::class);
|
||||
|
||||
Reference in New Issue
Block a user