Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
2026-01-26 13:04:07 +09:00
43 changed files with 2824 additions and 831 deletions

View File

@@ -84,6 +84,49 @@ ## 프로젝트 기술 스택
- **React 페이지**: React 18 + Babel (브라우저 트랜스파일링)
- **Database**: MySQL 8
---
## 데이터베이스 아키텍처 (필수 규칙)
> **경고: MNG 프로젝트에서는 마이그레이션 파일을 생성하지 않습니다!**
### 핵심 원칙
| 작업 | 올바른 위치 | MNG에서 |
|------|------------|---------|
| 마이그레이션 생성 | `/home/aweso/sam/api/database/migrations/` | ❌ 금지 |
| 마이그레이션 실행 | `docker exec sam-api-1 php artisan migrate` | ❌ 금지 |
| 테이블 생성/수정 | API 프로젝트에서만 | ❌ 금지 |
### MNG database 폴더 상태
```
/home/aweso/sam/mng/database/
├── migrations/ ← 비어있음 (파일 생성 금지!)
├── seeders/ ← MNG 전용 시더만 허용 (예: MngMenuSeeder)
└── factories/ ← 사용 안 함
```
### MNG에서 허용되는 것
- ✅ 컨트롤러, 뷰, 라우트 작성
- ✅ 모델 작성 (API의 테이블 사용)
- ✅ MNG 전용 시더 (MngMenuSeeder 등)
### MNG에서 금지되는 것
-`database/migrations/` 에 파일 생성
-`docker exec sam-mng-1 php artisan migrate` 실행
- ❌ 테이블 구조 변경 관련 작업
### 새 테이블이 필요할 때
1. API 프로젝트에서 마이그레이션 생성
2. `docker exec sam-api-1 php artisan migrate` 실행
3. MNG에서 해당 테이블의 모델만 작성
---
## HTMX 네비게이션 규칙
### HX-Redirect가 필요한 페이지

View File

@@ -0,0 +1,155 @@
<?php
namespace App\Http\Controllers\Sales;
use App\Http\Controllers\Controller;
use App\Models\Sales\SalesManager;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\View\View;
/**
* 영업 담당자/매니저 관리 컨트롤러
*/
class SalesManagerController extends Controller
{
/**
* 목록 페이지
*/
public function index(Request $request): View|Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('sales.managers.index'));
}
$query = SalesManager::query()->active();
// 검색
if ($search = $request->get('search')) {
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('member_id', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%")
->orWhere('phone', 'like', "%{$search}%");
});
}
// 역할 필터
if ($role = $request->get('role')) {
$query->where('role', $role);
}
$managers = $query->orderBy('name')->paginate(20);
// 통계
$stats = [
'total' => SalesManager::active()->count(),
'operators' => SalesManager::active()->where('role', 'operator')->count(),
'sales_admins' => SalesManager::active()->where('role', 'sales_admin')->count(),
'managers' => SalesManager::active()->where('role', 'manager')->count(),
];
return view('sales.managers.index', compact('managers', 'stats'));
}
/**
* 등록 폼
*/
public function create(): View
{
$parents = SalesManager::active()
->whereIn('role', ['operator', 'sales_admin'])
->orderBy('name')
->get();
return view('sales.managers.create', compact('parents'));
}
/**
* 등록 처리
*/
public function store(Request $request)
{
$validated = $request->validate([
'member_id' => 'required|string|max:50|unique:sales_managers,member_id',
'password' => 'required|string|min:4',
'name' => 'required|string|max:100',
'phone' => 'nullable|string|max:20',
'email' => 'nullable|email|max:100',
'parent_id' => 'nullable|exists:sales_managers,id',
'role' => 'required|in:operator,sales_admin,manager',
'remarks' => 'nullable|string',
]);
SalesManager::create($validated);
return redirect()->route('sales.managers.index')
->with('success', '담당자가 등록되었습니다.');
}
/**
* 상세 페이지
*/
public function show(int $id): View
{
$manager = SalesManager::with(['parent', 'children', 'registeredProspects', 'records'])
->findOrFail($id);
return view('sales.managers.show', compact('manager'));
}
/**
* 수정 폼
*/
public function edit(int $id): View
{
$manager = SalesManager::findOrFail($id);
$parents = SalesManager::active()
->whereIn('role', ['operator', 'sales_admin'])
->where('id', '!=', $id)
->orderBy('name')
->get();
return view('sales.managers.edit', compact('manager', 'parents'));
}
/**
* 수정 처리
*/
public function update(Request $request, int $id)
{
$manager = SalesManager::findOrFail($id);
$validated = $request->validate([
'name' => 'required|string|max:100',
'phone' => 'nullable|string|max:20',
'email' => 'nullable|email|max:100',
'parent_id' => 'nullable|exists:sales_managers,id',
'role' => 'required|in:operator,sales_admin,manager',
'remarks' => 'nullable|string',
'password' => 'nullable|string|min:4',
]);
// 비밀번호가 비어있으면 제외
if (empty($validated['password'])) {
unset($validated['password']);
}
$manager->update($validated);
return redirect()->route('sales.managers.index')
->with('success', '담당자 정보가 수정되었습니다.');
}
/**
* 삭제 처리 (비활성화)
*/
public function destroy(int $id)
{
$manager = SalesManager::findOrFail($id);
$manager->update(['is_active' => false]);
return redirect()->route('sales.managers.index')
->with('success', '담당자가 비활성화되었습니다.');
}
}

View File

@@ -0,0 +1,170 @@
<?php
namespace App\Http\Controllers\Sales;
use App\Http\Controllers\Controller;
use App\Models\Sales\SalesManager;
use App\Models\Sales\SalesProspect;
use App\Models\Sales\SalesProspectProduct;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\View\View;
/**
* 가망고객 관리 컨트롤러
*/
class SalesProspectController extends Controller
{
/**
* 목록 페이지
*/
public function index(Request $request): View|Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('sales.prospects.index'));
}
$query = SalesProspect::with(['manager', 'salesManager', 'products']);
// 검색
if ($search = $request->get('search')) {
$query->where(function ($q) use ($search) {
$q->where('company_name', 'like', "%{$search}%")
->orWhere('business_no', 'like', "%{$search}%")
->orWhere('representative', 'like', "%{$search}%")
->orWhere('contact_phone', 'like', "%{$search}%");
});
}
// 상태 필터
if ($status = $request->get('status')) {
$query->where('status', $status);
}
// 담당자 필터
if ($managerId = $request->get('manager_id')) {
$query->where(function ($q) use ($managerId) {
$q->where('manager_id', $managerId)
->orWhere('sales_manager_id', $managerId);
});
}
$prospects = $query->orderByDesc('created_at')->paginate(20);
// 통계
$stats = [
'total' => SalesProspect::count(),
'lead' => SalesProspect::where('status', 'lead')->count(),
'prospect' => SalesProspect::where('status', 'prospect')->count(),
'negotiation' => SalesProspect::where('status', 'negotiation')->count(),
'contracted' => SalesProspect::where('status', 'contracted')->count(),
'total_contract' => SalesProspectProduct::sum('contract_amount'),
'total_commission' => SalesProspectProduct::sum('commission_amount'),
];
$managers = SalesManager::active()->orderBy('name')->get();
return view('sales.prospects.index', compact('prospects', 'stats', 'managers'));
}
/**
* 등록 폼
*/
public function create(): View
{
$managers = SalesManager::active()->orderBy('name')->get();
return view('sales.prospects.create', compact('managers'));
}
/**
* 등록 처리
*/
public function store(Request $request)
{
$validated = $request->validate([
'manager_id' => 'required|exists:sales_managers,id',
'sales_manager_id' => 'nullable|exists:sales_managers,id',
'company_name' => 'required|string|max:200',
'representative' => 'nullable|string|max:100',
'business_no' => 'nullable|string|max:20',
'contact_phone' => 'nullable|string|max:20',
'email' => 'nullable|email|max:100',
'address' => 'nullable|string|max:500',
'status' => 'required|in:lead,prospect,negotiation,contracted,lost',
]);
$prospect = SalesProspect::create($validated);
return redirect()->route('sales.prospects.show', $prospect->id)
->with('success', '가망고객이 등록되었습니다.');
}
/**
* 상세 페이지
*/
public function show(int $id): View
{
$prospect = SalesProspect::with([
'manager',
'salesManager',
'products',
'scenarios',
'consultations.manager'
])->findOrFail($id);
$managers = SalesManager::active()
->whereIn('role', ['sales_admin', 'manager'])
->orderBy('name')
->get();
return view('sales.prospects.show', compact('prospect', 'managers'));
}
/**
* 수정 폼
*/
public function edit(int $id): View
{
$prospect = SalesProspect::findOrFail($id);
$managers = SalesManager::active()->orderBy('name')->get();
return view('sales.prospects.edit', compact('prospect', 'managers'));
}
/**
* 수정 처리
*/
public function update(Request $request, int $id)
{
$prospect = SalesProspect::findOrFail($id);
$validated = $request->validate([
'sales_manager_id' => 'nullable|exists:sales_managers,id',
'company_name' => 'required|string|max:200',
'representative' => 'nullable|string|max:100',
'business_no' => 'nullable|string|max:20',
'contact_phone' => 'nullable|string|max:20',
'email' => 'nullable|email|max:100',
'address' => 'nullable|string|max:500',
'status' => 'required|in:lead,prospect,negotiation,contracted,lost',
]);
$prospect->update($validated);
return redirect()->route('sales.prospects.show', $prospect->id)
->with('success', '가망고객 정보가 수정되었습니다.');
}
/**
* 삭제 처리
*/
public function destroy(int $id)
{
$prospect = SalesProspect::findOrFail($id);
$prospect->delete();
return redirect()->route('sales.prospects.index')
->with('success', '가망고객이 삭제되었습니다.');
}
}

View File

@@ -0,0 +1,166 @@
<?php
namespace App\Http\Controllers\Sales;
use App\Http\Controllers\Controller;
use App\Models\Sales\SalesManager;
use App\Models\Sales\SalesProspect;
use App\Models\Sales\SalesRecord;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\View\View;
/**
* 영업 실적 관리 컨트롤러
*/
class SalesRecordController extends Controller
{
/**
* 목록 페이지
*/
public function index(Request $request): View|Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('sales.records.index'));
}
$query = SalesRecord::with(['manager', 'prospect']);
// 검색
if ($search = $request->get('search')) {
$query->where(function ($q) use ($search) {
$q->where('description', 'like', "%{$search}%")
->orWhere('record_type', 'like', "%{$search}%")
->orWhereHas('prospect', function ($q2) use ($search) {
$q2->where('company_name', 'like', "%{$search}%");
});
});
}
// 상태 필터
if ($status = $request->get('status')) {
$query->where('status', $status);
}
// 담당자 필터
if ($managerId = $request->get('manager_id')) {
$query->where('manager_id', $managerId);
}
// 기간 필터
if ($startDate = $request->get('start_date')) {
$query->where('record_date', '>=', $startDate);
}
if ($endDate = $request->get('end_date')) {
$query->where('record_date', '<=', $endDate);
}
$records = $query->orderByDesc('record_date')->paginate(20);
// 통계
$stats = [
'total_count' => SalesRecord::count(),
'pending_count' => SalesRecord::pending()->count(),
'approved_count' => SalesRecord::approved()->count(),
'total_amount' => SalesRecord::sum('amount'),
'total_commission' => SalesRecord::sum('commission'),
'pending_amount' => SalesRecord::pending()->sum('amount'),
'approved_amount' => SalesRecord::approved()->sum('amount'),
];
$managers = SalesManager::active()->orderBy('name')->get();
return view('sales.records.index', compact('records', 'stats', 'managers'));
}
/**
* 등록 폼
*/
public function create(): View
{
$managers = SalesManager::active()->orderBy('name')->get();
$prospects = SalesProspect::orderBy('company_name')->get();
return view('sales.records.create', compact('managers', 'prospects'));
}
/**
* 등록 처리
*/
public function store(Request $request)
{
$validated = $request->validate([
'manager_id' => 'required|exists:sales_managers,id',
'prospect_id' => 'nullable|exists:sales_prospects,id',
'record_date' => 'required|date',
'record_type' => 'required|string|max:50',
'amount' => 'required|numeric|min:0',
'commission' => 'nullable|numeric|min:0',
'description' => 'nullable|string',
'status' => 'required|in:pending,approved,rejected,paid',
]);
SalesRecord::create($validated);
return redirect()->route('sales.records.index')
->with('success', '실적이 등록되었습니다.');
}
/**
* 상세 페이지
*/
public function show(int $id): View
{
$record = SalesRecord::with(['manager', 'prospect'])->findOrFail($id);
return view('sales.records.show', compact('record'));
}
/**
* 수정 폼
*/
public function edit(int $id): View
{
$record = SalesRecord::findOrFail($id);
$managers = SalesManager::active()->orderBy('name')->get();
$prospects = SalesProspect::orderBy('company_name')->get();
return view('sales.records.edit', compact('record', 'managers', 'prospects'));
}
/**
* 수정 처리
*/
public function update(Request $request, int $id)
{
$record = SalesRecord::findOrFail($id);
$validated = $request->validate([
'manager_id' => 'required|exists:sales_managers,id',
'prospect_id' => 'nullable|exists:sales_prospects,id',
'record_date' => 'required|date',
'record_type' => 'required|string|max:50',
'amount' => 'required|numeric|min:0',
'commission' => 'nullable|numeric|min:0',
'description' => 'nullable|string',
'status' => 'required|in:pending,approved,rejected,paid',
]);
$record->update($validated);
return redirect()->route('sales.records.index')
->with('success', '실적이 수정되었습니다.');
}
/**
* 삭제 처리
*/
public function destroy(int $id)
{
$record = SalesRecord::findOrFail($id);
$record->delete();
return redirect()->route('sales.records.index')
->with('success', '실적이 삭제되었습니다.');
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace App\Models\Sales;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class SalesManager extends Model
{
use SoftDeletes;
protected $table = 'sales_managers';
protected $fillable = [
'member_id',
'password',
'name',
'phone',
'email',
'parent_id',
'role',
'remarks',
'is_active',
];
protected $hidden = [
'password',
];
protected $casts = [
'is_active' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
/**
* 상위 관리자
*/
public function parent(): BelongsTo
{
return $this->belongsTo(SalesManager::class, 'parent_id');
}
/**
* 하위 관리자 목록
*/
public function children(): HasMany
{
return $this->hasMany(SalesManager::class, 'parent_id');
}
/**
* 등록한 가망고객
*/
public function registeredProspects(): HasMany
{
return $this->hasMany(SalesProspect::class, 'manager_id');
}
/**
* 담당 가망고객
*/
public function assignedProspects(): HasMany
{
return $this->hasMany(SalesProspect::class, 'sales_manager_id');
}
/**
* 영업 실적
*/
public function records(): HasMany
{
return $this->hasMany(SalesRecord::class, 'manager_id');
}
/**
* 상담 기록
*/
public function consultations(): HasMany
{
return $this->hasMany(SalesProspectConsultation::class, 'manager_id');
}
/**
* 역할 라벨
*/
public function getRoleLabelAttribute(): string
{
return match ($this->role) {
'operator' => '운영자',
'sales_admin' => '영업관리',
'manager' => '매니저',
default => $this->role,
};
}
/**
* 역할별 색상 클래스
*/
public function getRoleColorAttribute(): string
{
return match ($this->role) {
'operator' => 'bg-purple-100 text-purple-800',
'sales_admin' => 'bg-blue-100 text-blue-800',
'manager' => 'bg-green-100 text-green-800',
default => 'bg-gray-100 text-gray-800',
};
}
/**
* 활성 관리자만 조회
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace App\Models\Sales;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class SalesProspect extends Model
{
use SoftDeletes;
protected $table = 'sales_prospects';
protected $fillable = [
'manager_id',
'sales_manager_id',
'company_name',
'representative',
'business_no',
'contact_phone',
'email',
'address',
'status',
];
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
/**
* 등록한 영업 담당자
*/
public function manager(): BelongsTo
{
return $this->belongsTo(SalesManager::class, 'manager_id');
}
/**
* 담당 매니저
*/
public function salesManager(): BelongsTo
{
return $this->belongsTo(SalesManager::class, 'sales_manager_id');
}
/**
* 계약 상품 목록
*/
public function products(): HasMany
{
return $this->hasMany(SalesProspectProduct::class, 'prospect_id');
}
/**
* 시나리오 체크리스트
*/
public function scenarios(): HasMany
{
return $this->hasMany(SalesProspectScenario::class, 'prospect_id');
}
/**
* 상담 기록
*/
public function consultations(): HasMany
{
return $this->hasMany(SalesProspectConsultation::class, 'prospect_id');
}
/**
* 영업 실적
*/
public function records(): HasMany
{
return $this->hasMany(SalesRecord::class, 'prospect_id');
}
/**
* 상태 라벨
*/
public function getStatusLabelAttribute(): string
{
return match ($this->status) {
'lead' => '리드',
'prospect' => '가망',
'negotiation' => '협상중',
'contracted' => '계약완료',
'lost' => '실패',
default => $this->status,
};
}
/**
* 상태별 색상 클래스
*/
public function getStatusColorAttribute(): string
{
return match ($this->status) {
'lead' => 'bg-gray-100 text-gray-800',
'prospect' => 'bg-blue-100 text-blue-800',
'negotiation' => 'bg-yellow-100 text-yellow-800',
'contracted' => 'bg-green-100 text-green-800',
'lost' => 'bg-red-100 text-red-800',
default => 'bg-gray-100 text-gray-800',
};
}
/**
* 사업자번호 포맷팅 (XXX-XX-XXXXX)
*/
public function getFormattedBusinessNoAttribute(): string
{
$bizNo = preg_replace('/[^0-9]/', '', $this->business_no);
if (strlen($bizNo) === 10) {
return substr($bizNo, 0, 3) . '-' . substr($bizNo, 3, 2) . '-' . substr($bizNo, 5);
}
return $this->business_no ?? '';
}
/**
* 총 계약금액
*/
public function getTotalContractAmountAttribute(): float
{
return $this->products()->sum('contract_amount');
}
/**
* 총 수수료
*/
public function getTotalCommissionAttribute(): float
{
return $this->products()->sum('commission_amount');
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace App\Models\Sales;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class SalesProspectConsultation extends Model
{
use SoftDeletes;
protected $table = 'sales_prospect_consultations';
protected $fillable = [
'prospect_id',
'manager_id',
'scenario_type',
'step_id',
'log_text',
'audio_file_path',
'attachment_paths',
'consultation_type',
];
protected $casts = [
'step_id' => 'integer',
'attachment_paths' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
/**
* 가망고객
*/
public function prospect(): BelongsTo
{
return $this->belongsTo(SalesProspect::class, 'prospect_id');
}
/**
* 작성자
*/
public function manager(): BelongsTo
{
return $this->belongsTo(SalesManager::class, 'manager_id');
}
/**
* 상담 유형 라벨
*/
public function getConsultationTypeLabelAttribute(): string
{
return match ($this->consultation_type) {
'text' => '텍스트',
'audio' => '음성',
'file' => '파일',
default => $this->consultation_type,
};
}
/**
* 상담 유형별 아이콘 클래스
*/
public function getConsultationTypeIconAttribute(): string
{
return match ($this->consultation_type) {
'text' => 'fa-comment',
'audio' => 'fa-microphone',
'file' => 'fa-paperclip',
default => 'fa-question',
};
}
/**
* 첨부파일 개수
*/
public function getAttachmentCountAttribute(): int
{
return is_array($this->attachment_paths) ? count($this->attachment_paths) : 0;
}
/**
* 음성 파일 존재 여부
*/
public function getHasAudioAttribute(): bool
{
return !empty($this->audio_file_path);
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace App\Models\Sales;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class SalesProspectProduct extends Model
{
use SoftDeletes;
protected $table = 'sales_prospect_products';
protected $fillable = [
'prospect_id',
'product_name',
'contract_amount',
'subscription_fee',
'commission_rate',
'commission_amount',
'contract_date',
'join_approved',
'payment_approved',
'payout_rate',
'payout_amount',
'sub_models',
];
protected $casts = [
'contract_amount' => 'decimal:2',
'subscription_fee' => 'decimal:2',
'commission_rate' => 'decimal:2',
'commission_amount' => 'decimal:2',
'payout_rate' => 'decimal:2',
'payout_amount' => 'decimal:2',
'contract_date' => 'date',
'join_approved' => 'boolean',
'payment_approved' => 'boolean',
'sub_models' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
/**
* 가망고객
*/
public function prospect(): BelongsTo
{
return $this->belongsTo(SalesProspect::class, 'prospect_id');
}
/**
* 수수료 자동 계산
*/
public function calculateCommission(): void
{
$this->commission_amount = $this->contract_amount * ($this->commission_rate / 100);
}
/**
* 지급금액 계산
*/
public function calculatePayout(): void
{
$this->payout_amount = $this->contract_amount * ($this->payout_rate / 100);
}
/**
* 승인 상태 라벨
*/
public function getApprovalStatusAttribute(): string
{
if ($this->payment_approved) {
return '결제승인';
}
if ($this->join_approved) {
return '가입승인';
}
return '대기중';
}
/**
* 승인 상태별 색상
*/
public function getApprovalColorAttribute(): string
{
if ($this->payment_approved) {
return 'bg-green-100 text-green-800';
}
if ($this->join_approved) {
return 'bg-blue-100 text-blue-800';
}
return 'bg-gray-100 text-gray-800';
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Models\Sales;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SalesProspectScenario extends Model
{
protected $table = 'sales_prospect_scenarios';
protected $fillable = [
'prospect_id',
'scenario_type',
'step_id',
'checkpoint_index',
'is_checked',
];
protected $casts = [
'step_id' => 'integer',
'checkpoint_index' => 'integer',
'is_checked' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 가망고객
*/
public function prospect(): BelongsTo
{
return $this->belongsTo(SalesProspect::class, 'prospect_id');
}
/**
* 시나리오 유형 라벨
*/
public function getScenarioTypeLabelAttribute(): string
{
return match ($this->scenario_type) {
'sales' => '영업 시나리오',
'manager' => '매니저 시나리오',
default => $this->scenario_type,
};
}
/**
* 특정 가망고객의 시나리오 진행률 계산
*/
public static function getProgressRate(int $prospectId, string $scenarioType): float
{
$total = self::where('prospect_id', $prospectId)
->where('scenario_type', $scenarioType)
->count();
if ($total === 0) {
return 0;
}
$checked = self::where('prospect_id', $prospectId)
->where('scenario_type', $scenarioType)
->where('is_checked', true)
->count();
return round(($checked / $total) * 100, 1);
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace App\Models\Sales;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class SalesRecord extends Model
{
use SoftDeletes;
protected $table = 'sales_records';
protected $fillable = [
'manager_id',
'prospect_id',
'record_date',
'record_type',
'amount',
'commission',
'description',
'status',
];
protected $casts = [
'record_date' => 'date',
'amount' => 'decimal:2',
'commission' => 'decimal:2',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
/**
* 담당자
*/
public function manager(): BelongsTo
{
return $this->belongsTo(SalesManager::class, 'manager_id');
}
/**
* 가망고객
*/
public function prospect(): BelongsTo
{
return $this->belongsTo(SalesProspect::class, 'prospect_id');
}
/**
* 상태 라벨
*/
public function getStatusLabelAttribute(): string
{
return match ($this->status) {
'pending' => '대기',
'approved' => '승인',
'rejected' => '반려',
'paid' => '지급완료',
default => $this->status,
};
}
/**
* 상태별 색상 클래스
*/
public function getStatusColorAttribute(): string
{
return match ($this->status) {
'pending' => 'bg-yellow-100 text-yellow-800',
'approved' => 'bg-blue-100 text-blue-800',
'rejected' => 'bg-red-100 text-red-800',
'paid' => 'bg-green-100 text-green-800',
default => 'bg-gray-100 text-gray-800',
};
}
/**
* 특정 기간의 실적 합계
*/
public static function getSummary(int $managerId, ?string $startDate = null, ?string $endDate = null): array
{
$query = self::where('manager_id', $managerId);
if ($startDate) {
$query->where('record_date', '>=', $startDate);
}
if ($endDate) {
$query->where('record_date', '<=', $endDate);
}
return [
'total_amount' => $query->sum('amount'),
'total_commission' => $query->sum('commission'),
'record_count' => $query->count(),
'approved_amount' => (clone $query)->where('status', 'approved')->sum('amount'),
'paid_amount' => (clone $query)->where('status', 'paid')->sum('amount'),
];
}
/**
* 대기 상태만 조회
*/
public function scopePending($query)
{
return $query->where('status', 'pending');
}
/**
* 승인 상태만 조회
*/
public function scopeApproved($query)
{
return $query->where('status', 'approved');
}
}

View File

@@ -1,49 +0,0 @@
<?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('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};

View File

@@ -1,35 +0,0 @@
<?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('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->integer('expiration');
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};

View File

@@ -1,57 +0,0 @@
<?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('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};

View File

@@ -1,36 +0,0 @@
<?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('sales_scenario_checklists', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->unsignedTinyInteger('step_id')->comment('단계 ID (1-6)');
$table->unsignedTinyInteger('checkpoint_index')->comment('체크포인트 인덱스');
$table->boolean('is_checked')->default(true);
$table->timestamps();
// 복합 유니크 키: 사용자별, 단계별, 체크포인트별 하나의 레코드
$table->unique(['tenant_id', 'user_id', 'step_id', 'checkpoint_index'], 'sales_scenario_unique');
$table->index(['tenant_id', 'user_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sales_scenario_checklists');
}
};

View File

@@ -1,42 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* Design 시뮬레이터 동기화를 위한 items 테이블 필드 추가
*/
public function up(): void
{
Schema::table('items', function (Blueprint $table) {
// 공정유형: screen(스크린), bending(절곡), electric(전기), steel(철재), assembly(조립)
$table->string('process_type', 20)->nullable()->after('category_id')
->comment('공정유형: screen, bending, electric, steel, assembly');
// 품목분류: 원단, 패널, 도장, 표면처리, 가이드레일, 케이스, 모터, 제어반 등
$table->string('item_category', 50)->nullable()->after('process_type')
->comment('품목분류: 원단, 패널, 도장, 가이드레일, 모터 등');
// 인덱스 추가
$table->index('process_type', 'idx_items_process_type');
$table->index('item_category', 'idx_items_item_category');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('items', function (Blueprint $table) {
$table->dropIndex('idx_items_process_type');
$table->dropIndex('idx_items_item_category');
$table->dropColumn(['process_type', 'item_category']);
});
}
};

View File

@@ -1,53 +0,0 @@
<?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('category_groups', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
// 코드: area_based, weight_based, quantity_based
$table->string('code', 50)->comment('코드: area_based, weight_based, quantity_based');
// 이름: 면적기반, 중량기반, 수량기반
$table->string('name', 100)->comment('이름: 면적기반, 중량기반, 수량기반');
// 곱셈 변수: M(면적), K(중량), null(수량)
$table->string('multiplier_variable', 20)->nullable()
->comment('곱셈 변수: M(면적), K(중량), null(수량기반)');
// 소속 카테고리 목록 (JSON 배열)
$table->json('categories')->nullable()
->comment('소속 카테고리 목록: ["원단", "패널", "도장"]');
$table->text('description')->nullable();
$table->unsignedInteger('sort_order')->default(0);
$table->boolean('is_active')->default(true);
$table->timestamps();
// 인덱스
$table->index('tenant_id', 'idx_category_groups_tenant');
$table->unique(['tenant_id', 'code'], 'uq_category_groups_tenant_code');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('category_groups');
}
};

View File

@@ -1,44 +0,0 @@
<?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('barobill_members', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->string('biz_no', 20)->comment('사업자번호');
$table->string('corp_name', 100)->comment('상호명');
$table->string('ceo_name', 50)->comment('대표자명');
$table->string('addr', 255)->nullable()->comment('주소');
$table->string('biz_type', 50)->nullable()->comment('업태');
$table->string('biz_class', 50)->nullable()->comment('종목');
$table->string('barobill_id', 50)->comment('바로빌 아이디');
$table->string('barobill_pwd', 255)->comment('바로빌 비밀번호 (해시)');
$table->string('manager_name', 50)->nullable()->comment('담당자명');
$table->string('manager_email', 100)->nullable()->comment('담당자 이메일');
$table->string('manager_hp', 20)->nullable()->comment('담당자 전화번호');
$table->enum('status', ['active', 'inactive', 'pending'])->default('active')->comment('상태');
$table->timestamps();
$table->softDeletes();
$table->unique(['tenant_id', 'biz_no'], 'unique_tenant_biz_no');
$table->index('status');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('barobill_members');
}
};

View File

@@ -1,41 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* 바로빌 API 설정 테이블
* - 테스트/운영 서버별 인증키 및 URL 관리
*/
public function up(): void
{
Schema::create('barobill_configs', function (Blueprint $table) {
$table->id();
$table->string('name', 50)->comment('설정 이름 (예: 테스트서버, 운영서버)');
$table->enum('environment', ['test', 'production'])->comment('환경 (test/production)');
$table->string('cert_key', 100)->comment('CERTKEY (인증키)');
$table->string('corp_num', 20)->nullable()->comment('파트너 사업자번호');
$table->string('base_url', 255)->comment('API 서버 URL');
$table->text('description')->nullable()->comment('설명');
$table->boolean('is_active')->default(false)->comment('활성화 여부 (환경당 1개만 활성화)');
$table->timestamps();
$table->softDeletes();
// 환경별로 하나만 활성화 가능하도록 부분 유니크 인덱스 (is_active=true인 것 중에서)
$table->index(['environment', 'is_active']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('barobill_configs');
}
};

View File

@@ -1,32 +0,0 @@
<?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('barobill_settings', function (Blueprint $table) {
// 서비스 이용 옵션
$table->boolean('use_tax_invoice')->default(false)->after('auto_issue')->comment('전자세금계산서 이용');
$table->boolean('use_bank_account')->default(false)->after('use_tax_invoice')->comment('계좌조회 이용');
$table->boolean('use_card_usage')->default(false)->after('use_bank_account')->comment('카드사용내역 이용');
$table->boolean('use_hometax')->default(false)->after('use_card_usage')->comment('홈텍스매입/매출 이용');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('barobill_settings', function (Blueprint $table) {
$table->dropColumn(['use_tax_invoice', 'use_bank_account', 'use_card_usage', 'use_hometax']);
});
}
};

View File

@@ -1,34 +0,0 @@
<?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('coocon_configs', function (Blueprint $table) {
$table->id();
$table->string('name', 100)->comment('설정 이름');
$table->enum('environment', ['test', 'production'])->default('test')->comment('환경');
$table->string('api_key', 100)->comment('API 키');
$table->string('base_url', 255)->comment('API 기본 URL');
$table->text('description')->nullable()->comment('설명');
$table->boolean('is_active')->default(false)->comment('활성화 여부');
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('coocon_configs');
}
};

View File

@@ -1,57 +0,0 @@
<?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('credit_inquiries', function (Blueprint $table) {
$table->id();
$table->string('inquiry_key', 32)->unique()->comment('조회 고유 키');
$table->string('company_key', 20)->index()->comment('사업자번호/법인번호');
$table->string('company_name')->nullable()->comment('업체명');
$table->unsignedBigInteger('user_id')->nullable()->comment('조회자 ID');
$table->timestamp('inquired_at')->comment('조회 일시');
// 요약 정보 (빠른 조회용)
$table->unsignedInteger('short_term_overdue_cnt')->default(0)->comment('단기연체정보 건수');
$table->unsignedInteger('negative_info_kci_cnt')->default(0)->comment('신용도판단정보(한국신용정보원) 건수');
$table->unsignedInteger('negative_info_pb_cnt')->default(0)->comment('공공정보 건수');
$table->unsignedInteger('negative_info_cb_cnt')->default(0)->comment('신용도판단정보(신용정보사) 건수');
$table->unsignedInteger('suspension_info_cnt')->default(0)->comment('당좌거래정지정보 건수');
$table->unsignedInteger('workout_cnt')->default(0)->comment('법정관리/워크아웃정보 건수');
// API 응답 원본 데이터 (JSON)
$table->json('raw_summary')->nullable()->comment('OA12 신용요약정보 원본');
$table->json('raw_short_term_overdue')->nullable()->comment('OA13 단기연체정보 원본');
$table->json('raw_negative_info_kci')->nullable()->comment('OA14 신용도판단정보(한국신용정보원) 원본');
$table->json('raw_negative_info_cb')->nullable()->comment('OA15 신용도판단정보(신용정보사) 원본');
$table->json('raw_suspension_info')->nullable()->comment('OA16 당좌거래정지정보 원본');
$table->json('raw_workout_info')->nullable()->comment('OA17 법정관리/워크아웃정보 원본');
// 상태
$table->enum('status', ['success', 'partial', 'failed'])->default('success')->comment('조회 상태');
$table->text('error_message')->nullable()->comment('에러 메시지');
$table->timestamps();
// 인덱스
$table->index(['company_key', 'inquired_at']);
$table->index('inquired_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('credit_inquiries');
}
};

View File

@@ -1,55 +0,0 @@
<?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('credit_inquiries', function (Blueprint $table) {
// 기업 기본정보 (OA08)
$table->string('ceo_name')->nullable()->after('company_name')->comment('대표자명');
$table->string('company_address')->nullable()->after('ceo_name')->comment('회사 주소');
$table->string('business_type')->nullable()->after('company_address')->comment('업종');
$table->string('business_item')->nullable()->after('business_type')->comment('업태');
$table->date('establishment_date')->nullable()->after('business_item')->comment('설립일');
// 국세청 사업자등록 상태
$table->string('nts_status', 20)->nullable()->after('establishment_date')->comment('국세청 상태 (영업/휴업/폐업)');
$table->string('nts_status_code', 2)->nullable()->after('nts_status')->comment('국세청 상태코드 (01/02/03)');
$table->string('nts_tax_type', 50)->nullable()->after('nts_status_code')->comment('과세유형');
$table->date('nts_closure_date')->nullable()->after('nts_tax_type')->comment('폐업일');
// API 원본 데이터
$table->json('raw_company_info')->nullable()->after('raw_workout_info')->comment('OA08 기업기본정보 원본');
$table->json('raw_nts_status')->nullable()->after('raw_company_info')->comment('국세청 상태조회 원본');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('credit_inquiries', function (Blueprint $table) {
$table->dropColumn([
'ceo_name',
'company_address',
'business_type',
'business_item',
'establishment_date',
'nts_status',
'nts_status_code',
'nts_tax_type',
'nts_closure_date',
'raw_company_info',
'raw_nts_status',
]);
});
}
};

View File

@@ -1,53 +0,0 @@
<?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('barobill_bank_transactions', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
$table->string('bank_account_num', 50)->comment('계좌번호');
$table->string('bank_code', 10)->nullable()->comment('은행코드');
$table->string('bank_name', 50)->nullable()->comment('은행명');
$table->string('trans_date', 8)->comment('거래일 (YYYYMMDD)');
$table->string('trans_time', 6)->nullable()->comment('거래시간 (HHMMSS)');
$table->string('trans_dt', 20)->comment('거래일시 원본 (YYYYMMDDHHMMSS)');
$table->decimal('deposit', 18, 2)->default(0)->comment('입금액');
$table->decimal('withdraw', 18, 2)->default(0)->comment('출금액');
$table->decimal('balance', 18, 2)->default(0)->comment('잔액');
$table->string('summary', 255)->nullable()->comment('적요');
$table->string('cast', 100)->nullable()->comment('상대방');
$table->string('memo', 255)->nullable()->comment('메모');
$table->string('trans_office', 100)->nullable()->comment('거래점');
$table->string('account_code', 50)->nullable()->comment('계정과목 코드');
$table->string('account_name', 100)->nullable()->comment('계정과목 명');
$table->timestamps();
// 복합 유니크 인덱스: 같은 거래는 중복 저장 방지
$table->unique(
['tenant_id', 'bank_account_num', 'trans_dt', 'deposit', 'withdraw', 'balance'],
'barobill_bank_trans_unique'
);
// 조회용 인덱스
$table->index(['tenant_id', 'trans_date'], 'bb_trans_tenant_date_idx');
$table->index(['tenant_id', 'bank_account_num', 'trans_date'], 'bb_trans_tenant_acct_date_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('barobill_bank_transactions');
}
};

View File

@@ -1,37 +0,0 @@
<?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('account_codes', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
$table->string('code', 10)->comment('계정과목 코드');
$table->string('name', 100)->comment('계정과목 명');
$table->string('category', 50)->nullable()->comment('분류 (자산/부채/자본/수익/비용)');
$table->integer('sort_order')->default(0)->comment('정렬순서');
$table->boolean('is_active')->default(true)->comment('사용여부');
$table->timestamps();
// 테넌트별 계정과목 코드 유니크
$table->unique(['tenant_id', 'code'], 'account_codes_tenant_code_unique');
$table->index(['tenant_id', 'is_active'], 'account_codes_tenant_active_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('account_codes');
}
};

View File

@@ -1,61 +0,0 @@
<?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('barobill_card_transactions', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
$table->string('card_num', 50)->comment('카드번호');
$table->string('card_company', 10)->nullable()->comment('카드사 코드');
$table->string('card_company_name', 50)->nullable()->comment('카드사명');
$table->string('use_dt', 20)->comment('사용일시 원본 (YYYYMMDDHHMMSS)');
$table->string('use_date', 8)->comment('사용일 (YYYYMMDD)');
$table->string('use_time', 6)->nullable()->comment('사용시간 (HHMMSS)');
$table->string('approval_num', 50)->nullable()->comment('승인번호');
$table->string('approval_type', 10)->nullable()->comment('승인유형 (1=승인, 2=취소)');
$table->decimal('approval_amount', 18, 2)->default(0)->comment('승인금액');
$table->decimal('tax', 18, 2)->default(0)->comment('부가세');
$table->decimal('service_charge', 18, 2)->default(0)->comment('봉사료');
$table->string('payment_plan', 10)->nullable()->comment('할부개월수');
$table->string('currency_code', 10)->default('KRW')->comment('통화코드');
$table->string('merchant_name', 255)->nullable()->comment('가맹점명');
$table->string('merchant_biz_num', 20)->nullable()->comment('가맹점 사업자번호');
$table->string('merchant_addr', 255)->nullable()->comment('가맹점 주소');
$table->string('merchant_ceo', 100)->nullable()->comment('가맹점 대표자');
$table->string('merchant_biz_type', 100)->nullable()->comment('가맹점 업종');
$table->string('merchant_tel', 50)->nullable()->comment('가맹점 전화번호');
$table->string('memo', 255)->nullable()->comment('메모');
$table->string('use_key', 100)->nullable()->comment('사용키');
$table->string('account_code', 50)->nullable()->comment('계정과목 코드');
$table->string('account_name', 100)->nullable()->comment('계정과목 명');
$table->timestamps();
// 복합 유니크 인덱스: 같은 거래는 중복 저장 방지
$table->unique(
['tenant_id', 'card_num', 'use_dt', 'approval_num', 'approval_amount'],
'bb_card_trans_unique'
);
// 조회용 인덱스
$table->index(['tenant_id', 'use_date'], 'bb_card_trans_tenant_date_idx');
$table->index(['tenant_id', 'card_num', 'use_date'], 'bb_card_trans_tenant_card_date_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('barobill_card_transactions');
}
};

View File

@@ -1,52 +0,0 @@
<?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('barobill_card_transaction_splits', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
// 원본 거래 고유키 (바로빌에서 가져온 원본 데이터 식별)
$table->string('original_unique_key', 200)->comment('원본 거래 고유키 (cardNum|useDt|approvalNum|amount)');
// 분개 정보
$table->decimal('split_amount', 18, 2)->comment('분개 금액');
$table->string('account_code', 50)->nullable()->comment('계정과목 코드');
$table->string('account_name', 100)->nullable()->comment('계정과목명');
$table->string('memo', 255)->nullable()->comment('분개 메모');
$table->integer('sort_order')->default(0)->comment('정렬 순서');
// 원본 거래 정보 (조회 편의를 위해 저장)
$table->string('card_num', 50)->comment('카드번호');
$table->string('use_dt', 20)->comment('사용일시 (YYYYMMDDHHMMSS)');
$table->string('use_date', 8)->comment('사용일 (YYYYMMDD)');
$table->string('approval_num', 50)->nullable()->comment('승인번호');
$table->decimal('original_amount', 18, 2)->comment('원본 거래 총액');
$table->string('merchant_name', 255)->nullable()->comment('가맹점명');
$table->timestamps();
// 인덱스
$table->index(['tenant_id', 'original_unique_key'], 'bb_card_split_tenant_key_idx');
$table->index(['tenant_id', 'use_date'], 'bb_card_split_tenant_date_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('barobill_card_transaction_splits');
}
};

View File

@@ -1,34 +0,0 @@
<?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('barobill_card_transactions', function (Blueprint $table) {
// 공제 유형: 공제(deductible) / 불공(non_deductible)
$table->string('deduction_type', 20)->nullable()->after('account_name')->comment('공제유형: deductible/non_deductible');
// 증빙/판매자상호 (사용자 수정용)
$table->string('evidence_name', 255)->nullable()->after('deduction_type')->comment('증빙/판매자상호 (수정용)');
// 내역 (사용자 수정용)
$table->string('description', 500)->nullable()->after('evidence_name')->comment('내역 (수정용)');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('barobill_card_transactions', function (Blueprint $table) {
$table->dropColumn(['deduction_type', 'evidence_name', 'description']);
});
}
};

View File

@@ -1,30 +0,0 @@
<?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('barobill_card_transaction_splits', function (Blueprint $table) {
$table->string('evidence_name', 255)->nullable()->after('account_name')->comment('증빙/판매자상호');
$table->string('description', 500)->nullable()->after('evidence_name')->comment('내역');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('barobill_card_transaction_splits', function (Blueprint $table) {
$table->dropColumn(['evidence_name', 'description']);
});
}
};

View File

@@ -1,29 +0,0 @@
<?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('barobill_card_transaction_splits', function (Blueprint $table) {
$table->string('deduction_type', 20)->nullable()->after('account_name')->comment('공제유형: deductible/non_deductible');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('barobill_card_transaction_splits', function (Blueprint $table) {
$table->dropColumn('deduction_type');
});
}
};

View File

@@ -639,6 +639,46 @@ protected function seedMainMenus(): void
'options' => ['route_name' => 'credit.inquiry.index', 'section' => 'main'],
]);
// ========================================
// 영업관리 그룹
// ========================================
$salesGroup = $this->createMenu([
'name' => '영업관리',
'url' => '#',
'icon' => 'briefcase',
'sort_order' => $sortOrder++,
'options' => [
'section' => 'main',
'meta' => ['group_id' => 'sales-group'],
],
]);
$salesSubOrder = 0;
$this->createMenu([
'parent_id' => $salesGroup->id,
'name' => '영업담당자 관리',
'url' => '/sales/managers',
'icon' => 'users',
'sort_order' => $salesSubOrder++,
'options' => ['route_name' => 'sales.managers.index', 'section' => 'main'],
]);
$this->createMenu([
'parent_id' => $salesGroup->id,
'name' => '가망고객 관리',
'url' => '/sales/prospects',
'icon' => 'user-group',
'sort_order' => $salesSubOrder++,
'options' => ['route_name' => 'sales.prospects.index', 'section' => 'main'],
]);
$this->createMenu([
'parent_id' => $salesGroup->id,
'name' => '영업실적 관리',
'url' => '/sales/records',
'icon' => 'chart-bar',
'sort_order' => $salesSubOrder++,
'options' => ['route_name' => 'sales.records.index', 'section' => 'main'],
]);
// ========================================
// 시스템 그룹
// ========================================

View File

@@ -0,0 +1,153 @@
@extends('layouts.app')
@section('title', '영업담당자 등록')
@section('content')
<div class="max-w-2xl mx-auto">
<!-- 페이지 헤더 -->
<div class="mb-6">
<a href="{{ route('sales.managers.index') }}" 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>
<div class="flex items-center gap-3">
<h1 class="text-2xl font-bold text-gray-800">영업담당자 등록</h1>
<button type="button" id="fillTestDataBtn"
class="p-2 bg-amber-50 text-amber-600 rounded-lg hover:bg-amber-100 transition-all border border-amber-200 group relative"
title="샘플 데이터 자동 입력">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span class="absolute -top-10 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-50">
랜덤 데이터 채우기
</span>
</button>
</div>
</div>
<!-- -->
<form action="{{ route('sales.managers.store') }}" method="POST" id="managerForm" class="bg-white rounded-lg shadow-sm p-6 space-y-6">
@csrf
<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">로그인 ID <span class="text-red-500">*</span></label>
<input type="text" name="member_id" id="member_id" value="{{ old('member_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('member_id') border-red-500 @enderror">
@error('member_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">비밀번호 <span class="text-red-500">*</span></label>
<input type="text" name="password" id="password" 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('password') border-red-500 @enderror">
@error('password')
<p class="mt-1 text-sm text-red-500">{{ $message }}</p>
@enderror
</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">이름 <span class="text-red-500">*</span></label>
<input type="text" name="name" id="name" value="{{ old('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('name') border-red-500 @enderror">
@error('name')
<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">역할 <span class="text-red-500">*</span></label>
<select name="role" id="role" 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('role') border-red-500 @enderror">
<option value="manager" {{ old('role') === 'manager' ? 'selected' : '' }}>매니저</option>
<option value="sales_admin" {{ old('role') === 'sales_admin' ? 'selected' : '' }}>영업관리</option>
<option value="operator" {{ old('role') === 'operator' ? 'selected' : '' }}>운영자</option>
</select>
@error('role')
<p class="mt-1 text-sm text-red-500">{{ $message }}</p>
@enderror
</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="phone" id="phone" value="{{ old('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>
<label class="block text-sm font-medium text-gray-700 mb-2">이메일</label>
<input type="email" name="email" id="email" value="{{ old('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>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">상위 관리자</label>
<select name="parent_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($parents as $parent)
<option value="{{ $parent->id }}" {{ old('parent_id') == $parent->id ? 'selected' : '' }}>
{{ $parent->name }} ({{ $parent->role_label }})
</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">비고</label>
<textarea name="remarks" id="remarks" 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('remarks') }}</textarea>
</div>
<div class="flex justify-end gap-3 pt-4 border-t">
<a href="{{ route('sales.managers.index') }}"
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition">
취소
</a>
<button type="submit"
class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
등록
</button>
</div>
</form>
</div>
@endsection
@push('scripts')
<script>
document.getElementById('fillTestDataBtn').addEventListener('click', function() {
const sampleNames = ['한효주', '공유', '이동욱', '김고은', '신세경', '박서준', '김수현', '수지', '남주혁', '성시경', '아이유', '조세호'];
const sampleIds = ['sales_pro', 'mkt_king', 'deal_maker', 'win_win', 'growth_lab', 'biz_hero', 'ace_mgr', 'top_tier'];
const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
const randomNum = (len) => Array.from({length: len}, () => Math.floor(Math.random() * 10)).join('');
const formatPhone = (val) => {
const clean = val.replace(/[^0-9]/g, '');
if (clean.length === 11) {
return clean.slice(0, 3) + '-' + clean.slice(3, 7) + '-' + clean.slice(7);
}
return val;
};
const name = randomItem(sampleNames);
const idSuffix = randomNum(3);
document.getElementById('member_id').value = randomItem(sampleIds) + idSuffix;
document.getElementById('password').value = 'password123';
document.getElementById('name').value = name;
document.getElementById('phone').value = formatPhone('010' + randomNum(8));
document.getElementById('email').value = 'manager_' + randomNum(4) + '@example.com';
document.getElementById('remarks').value = 'MNG 시스템 생성 샘플 데이터';
});
</script>
@endpush

View File

@@ -0,0 +1,108 @@
@extends('layouts.app')
@section('title', '영업담당자 수정')
@section('content')
<div class="max-w-2xl mx-auto">
<!-- 페이지 헤더 -->
<div class="mb-6">
<a href="{{ route('sales.managers.index') }}" 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">{{ $manager->name }} ({{ $manager->member_id }})</p>
</div>
<!-- -->
<form action="{{ route('sales.managers.update', $manager->id) }}" method="POST" class="bg-white rounded-lg shadow-sm p-6 space-y-6">
@csrf
@method('PUT')
<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">로그인 ID</label>
<input type="text" value="{{ $manager->member_id }}" disabled
class="w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-100 text-gray-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">비밀번호</label>
<input type="password" name="password" placeholder="변경 시에만 입력"
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">비밀번호를 변경하려면 입력하세요.</p>
</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">이름 <span class="text-red-500">*</span></label>
<input type="text" name="name" value="{{ old('name', $manager->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('name') border-red-500 @enderror">
@error('name')
<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">역할 <span class="text-red-500">*</span></label>
<select name="role" 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('role') border-red-500 @enderror">
<option value="manager" {{ old('role', $manager->role) === 'manager' ? 'selected' : '' }}>매니저</option>
<option value="sales_admin" {{ old('role', $manager->role) === 'sales_admin' ? 'selected' : '' }}>영업관리</option>
<option value="operator" {{ old('role', $manager->role) === 'operator' ? 'selected' : '' }}>운영자</option>
</select>
@error('role')
<p class="mt-1 text-sm text-red-500">{{ $message }}</p>
@enderror
</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="phone" value="{{ old('phone', $manager->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>
<label class="block text-sm font-medium text-gray-700 mb-2">이메일</label>
<input type="email" name="email" value="{{ old('email', $manager->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>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">상위 관리자</label>
<select name="parent_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($parents as $parent)
<option value="{{ $parent->id }}" {{ old('parent_id', $manager->parent_id) == $parent->id ? 'selected' : '' }}>
{{ $parent->name }} ({{ $parent->role_label }})
</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">비고</label>
<textarea name="remarks" 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('remarks', $manager->remarks) }}</textarea>
</div>
<div class="flex justify-end gap-3 pt-4 border-t">
<a href="{{ route('sales.managers.index') }}"
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition">
취소
</a>
<button type="submit"
class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
수정
</button>
</div>
</form>
</div>
@endsection

View File

@@ -0,0 +1,137 @@
@extends('layouts.app')
@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>
</div>
<a href="{{ route('sales.managers.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 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-2xl font-bold text-gray-800">{{ number_format($stats['total']) }}</div>
</div>
<div class="bg-purple-50 rounded-lg shadow-sm p-4">
<div class="text-sm text-purple-600">운영자</div>
<div class="text-2xl font-bold text-purple-800">{{ number_format($stats['operators']) }}</div>
</div>
<div class="bg-blue-50 rounded-lg shadow-sm p-4">
<div class="text-sm text-blue-600">영업관리</div>
<div class="text-2xl font-bold text-blue-800">{{ number_format($stats['sales_admins']) }}</div>
</div>
<div class="bg-green-50 rounded-lg shadow-sm p-4">
<div class="text-sm text-green-600">매니저</div>
<div class="text-2xl font-bold text-green-800">{{ number_format($stats['managers']) }}</div>
</div>
</div>
<!-- 필터 영역 -->
<div class="flex-shrink-0 mb-4">
<form method="GET" class="flex flex-wrap gap-2 sm:gap-4 items-center bg-white p-4 rounded-lg shadow-sm">
<div class="flex-1 min-w-0 w-full sm:w-auto">
<input type="text"
name="search"
value="{{ request('search') }}"
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="role" 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="operator" {{ request('role') === 'operator' ? 'selected' : '' }}>운영자</option>
<option value="sales_admin" {{ request('role') === 'sales_admin' ? 'selected' : '' }}>영업관리</option>
<option value="manager" {{ request('role') === 'manager' ? '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">
검색
</button>
</form>
</div>
<!-- 테이블 -->
<div class="bg-white rounded-lg shadow-sm overflow-hidden flex-1 flex flex-col min-h-0">
<div class="overflow-x-auto">
<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>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@forelse($managers as $manager)
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="font-medium text-gray-900">{{ $manager->name }}</div>
@if($manager->email)
<div class="text-sm text-gray-500">{{ $manager->email }}</div>
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ $manager->member_id }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-1 text-xs font-medium rounded-full {{ $manager->role_color }}">
{{ $manager->role_label }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $manager->phone ?? '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $manager->parent?->name ?? '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $manager->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.managers.show', $manager->id) }}" class="text-blue-600 hover:text-blue-900 mr-3">상세</a>
<a href="{{ route('sales.managers.edit', $manager->id) }}" class="text-indigo-600 hover:text-indigo-900 mr-3">수정</a>
<form action="{{ route('sales.managers.destroy', $manager->id) }}" method="POST" class="inline"
onsubmit="return confirm('정말 비활성화하시겠습니까?')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-900">삭제</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-6 py-12 text-center text-gray-500">
등록된 담당자가 없습니다.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- 페이지네이션 -->
@if($managers->hasPages())
<div class="px-6 py-4 border-t border-gray-200">
{{ $managers->withQueryString()->links() }}
</div>
@endif
</div>
</div>
@endsection

View File

@@ -0,0 +1,128 @@
@extends('layouts.app')
@section('title', '영업담당자 상세')
@section('content')
<div class="max-w-4xl mx-auto">
<!-- 페이지 헤더 -->
<div class="mb-6 flex justify-between items-start">
<div>
<a href="{{ route('sales.managers.index') }}" 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">{{ $manager->name }}</h1>
<p class="text-sm text-gray-500 mt-1">
<span class="px-2 py-1 text-xs font-medium rounded-full {{ $manager->role_color }}">
{{ $manager->role_label }}
</span>
</p>
</div>
<a href="{{ route('sales.managers.edit', $manager->id) }}"
class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transition">
수정
</a>
</div>
<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>
<dl class="space-y-3">
<div class="flex justify-between">
<dt class="text-gray-500">로그인 ID</dt>
<dd class="font-medium text-gray-900">{{ $manager->member_id }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">이름</dt>
<dd class="font-medium text-gray-900">{{ $manager->name }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">전화번호</dt>
<dd class="font-medium text-gray-900">{{ $manager->phone ?? '-' }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">이메일</dt>
<dd class="font-medium text-gray-900">{{ $manager->email ?? '-' }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">상위 관리자</dt>
<dd class="font-medium text-gray-900">{{ $manager->parent?->name ?? '-' }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">등록일</dt>
<dd class="font-medium text-gray-900">{{ $manager->created_at->format('Y-m-d H:i') }}</dd>
</div>
</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="grid grid-cols-2 gap-4">
<div class="bg-blue-50 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-blue-800">{{ $manager->registeredProspects->count() }}</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">{{ $manager->assignedProspects->count() }}</div>
<div class="text-sm text-green-600">담당 가망고객</div>
</div>
<div class="bg-purple-50 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-purple-800">{{ $manager->records->count() }}</div>
<div class="text-sm text-purple-600">영업 실적</div>
</div>
<div class="bg-yellow-50 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-yellow-800">{{ $manager->children->count() }}</div>
<div class="text-sm text-yellow-600">하위 담당자</div>
</div>
</div>
</div>
</div>
<!-- 비고 -->
@if($manager->remarks)
<div class="mt-6 bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">비고</h2>
<p class="text-gray-700 whitespace-pre-line">{{ $manager->remarks }}</p>
</div>
@endif
<!-- 하위 담당자 목록 -->
@if($manager->children->isNotEmpty())
<div class="mt-6 bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">하위 담당자</h2>
<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-left 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($manager->children as $child)
<tr>
<td class="px-4 py-3">
<a href="{{ route('sales.managers.show', $child->id) }}" class="text-blue-600 hover:underline">
{{ $child->name }}
</a>
</td>
<td class="px-4 py-3">
<span class="px-2 py-1 text-xs font-medium rounded-full {{ $child->role_color }}">
{{ $child->role_label }}
</span>
</td>
<td class="px-4 py-3 text-gray-500">{{ $child->phone ?? '-' }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,121 @@
@extends('layouts.app')
@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">
<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>
</div>
<!-- -->
<form action="{{ route('sales.prospects.store') }}" method="POST" class="bg-white rounded-lg shadow-sm p-6 space-y-6">
@csrf
<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>
<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')
<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="text" name="business_no" value="{{ old('business_no') }}" placeholder="000-00-00000"
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') }}"
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>
<label class="block text-sm font-medium text-gray-700 mb-2">주소</label>
<input type="text" name="address" value="{{ old('address') }}"
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>
<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>
<div class="flex justify-end gap-3 pt-4 border-t">
<a href="{{ route('sales.prospects.index') }}"
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition">
취소
</a>
<button type="submit"
class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
등록
</button>
</div>
</form>
</div>
@endsection

View File

@@ -0,0 +1,106 @@
@extends('layouts.app')
@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">
<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>
</div>
<!-- -->
<form action="{{ route('sales.prospects.update', $prospect->id) }}" method="POST" class="bg-white rounded-lg shadow-sm p-6 space-y-6">
@csrf
@method('PUT')
<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>
<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')
<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="text" name="business_no" value="{{ old('business_no', $prospect->business_no) }}" placeholder="000-00-00000"
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) }}"
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>
<label class="block text-sm font-medium text-gray-700 mb-2">주소</label>
<input type="text" name="address" value="{{ old('address', $prospect->address) }}"
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>
<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>
</div>
<div class="flex justify-end gap-3 pt-4 border-t">
<a href="{{ route('sales.prospects.index') }}"
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition">
취소
</a>
<button type="submit"
class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
수정
</button>
</div>
</form>
</div>
@endsection

View File

@@ -0,0 +1,172 @@
@extends('layouts.app')
@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>
</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="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>
<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>
<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>
</div>
<!-- 필터 영역 -->
<div class="flex-shrink-0 mb-4">
<form method="GET" class="flex flex-wrap gap-2 sm:gap-4 items-center bg-white p-4 rounded-lg shadow-sm">
<div class="flex-1 min-w-0 w-full sm:w-auto">
<input type="text"
name="search"
value="{{ request('search') }}"
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
</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">
검색
</button>
</form>
</div>
<!-- 테이블 -->
<div class="bg-white rounded-lg shadow-sm overflow-hidden flex-1 flex flex-col min-h-0">
<div class="overflow-x-auto">
<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-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-right text-xs font-medium text-gray-500 uppercase tracking-wider">관리</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@forelse($prospects as $prospect)
<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>
@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>
</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
</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>
@else
<span class="text-gray-400">-</span>
@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>
<a href="{{ route('sales.prospects.edit', $prospect->id) }}" class="text-indigo-600 hover:text-indigo-900 mr-3">수정</a>
<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>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-6 py-12 text-center text-gray-500">
등록된 가망고객이 없습니다.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- 페이지네이션 -->
@if($prospects->hasPages())
<div class="px-6 py-4 border-t border-gray-200">
{{ $prospects->withQueryString()->links() }}
</div>
@endif
</div>
</div>
@endsection

View File

@@ -0,0 +1,173 @@
@extends('layouts.app')
@section('title', '가망고객 상세')
@section('content')
<div class="max-w-5xl mx-auto">
<!-- 페이지 헤더 -->
<div class="mb-6 flex justify-between items-start">
<div>
<a href="{{ route('sales.prospects.index') }}" 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">{{ $prospect->company_name }}</h1>
<p class="text-sm text-gray-500 mt-1">
<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
</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>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 기본 정보 -->
<div class="bg-white rounded-lg shadow-sm p-6">
<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->representative ?? '-' }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">연락처</dt>
<dd class="font-medium text-gray-900">{{ $prospect->contact_phone ?? '-' }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">이메일</dt>
<dd class="font-medium text-gray-900">{{ $prospect->email ?? '-' }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">주소</dt>
<dd class="font-medium text-gray-900 text-right">{{ $prospect->address ?? '-' }}</dd>
</div>
</dl>
</div>
<!-- 담당자 정보 -->
<div class="bg-white rounded-lg shadow-sm p-6">
<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>
</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>
</div>
</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>
<!-- 계약 상품 목록 -->
<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>
</div>
@else
<p class="text-center text-gray-500 py-8">등록된 계약 상품이 없습니다.</p>
@endif
</div>
<!-- 상담 기록 -->
<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
</div>
@else
<p class="text-center text-gray-500 py-8">등록된 상담 기록이 없습니다.</p>
@endif
</div>
</div>
@endsection

View File

@@ -0,0 +1,119 @@
@extends('layouts.app')
@section('title', '영업실적 등록')
@section('content')
<div class="max-w-2xl mx-auto">
<!-- 페이지 헤더 -->
<div class="mb-6">
<a href="{{ route('sales.records.index') }}" 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>
</div>
<!-- -->
<form action="{{ route('sales.records.store') }}" method="POST" class="bg-white rounded-lg shadow-sm p-6 space-y-6">
@csrf
<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>
<input type="date" name="record_date" value="{{ old('record_date', date('Y-m-d')) }}" 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('record_date') border-red-500 @enderror">
@error('record_date')
<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">실적 유형 <span class="text-red-500">*</span></label>
<input type="text" name="record_type" value="{{ old('record_type') }}" required placeholder="예: 신규계약, 갱신, 추가매출"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 @error('record_type') border-red-500 @enderror">
@error('record_type')
<p class="mt-1 text-sm text-red-500">{{ $message }}</p>
@enderror
</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">담당자 <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>
<select name="prospect_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($prospects as $prospect)
<option value="{{ $prospect->id }}" {{ old('prospect_id') == $prospect->id ? 'selected' : '' }}>
{{ $prospect->company_name }}
</option>
@endforeach
</select>
</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">실적 금액 <span class="text-red-500">*</span></label>
<input type="number" name="amount" value="{{ old('amount', 0) }}" required min="0"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 @error('amount') border-red-500 @enderror">
@error('amount')
<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="number" name="commission" value="{{ old('commission', 0) }}" min="0"
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>
<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="pending" {{ old('status') === 'pending' ? 'selected' : '' }}>대기</option>
<option value="approved" {{ old('status') === 'approved' ? 'selected' : '' }}>승인</option>
<option value="rejected" {{ old('status') === 'rejected' ? 'selected' : '' }}>반려</option>
<option value="paid" {{ old('status') === 'paid' ? 'selected' : '' }}>지급완료</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">설명</label>
<textarea name="description" 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('description') }}</textarea>
</div>
<div class="flex justify-end gap-3 pt-4 border-t">
<a href="{{ route('sales.records.index') }}"
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition">
취소
</a>
<button type="submit"
class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
등록
</button>
</div>
</form>
</div>
@endsection

View File

@@ -0,0 +1,120 @@
@extends('layouts.app')
@section('title', '영업실적 수정')
@section('content')
<div class="max-w-2xl mx-auto">
<!-- 페이지 헤더 -->
<div class="mb-6">
<a href="{{ route('sales.records.index') }}" 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>
</div>
<!-- -->
<form action="{{ route('sales.records.update', $record->id) }}" method="POST" class="bg-white rounded-lg shadow-sm p-6 space-y-6">
@csrf
@method('PUT')
<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>
<input type="date" name="record_date" value="{{ old('record_date', $record->record_date->format('Y-m-d')) }}" 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('record_date') border-red-500 @enderror">
@error('record_date')
<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">실적 유형 <span class="text-red-500">*</span></label>
<input type="text" name="record_type" value="{{ old('record_type', $record->record_type) }}" 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('record_type') border-red-500 @enderror">
@error('record_type')
<p class="mt-1 text-sm text-red-500">{{ $message }}</p>
@enderror
</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">담당자 <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', $record->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>
<select name="prospect_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($prospects as $prospect)
<option value="{{ $prospect->id }}" {{ old('prospect_id', $record->prospect_id) == $prospect->id ? 'selected' : '' }}>
{{ $prospect->company_name }}
</option>
@endforeach
</select>
</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">실적 금액 <span class="text-red-500">*</span></label>
<input type="number" name="amount" value="{{ old('amount', $record->amount) }}" required min="0"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 @error('amount') border-red-500 @enderror">
@error('amount')
<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="number" name="commission" value="{{ old('commission', $record->commission) }}" min="0"
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>
<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="pending" {{ old('status', $record->status) === 'pending' ? 'selected' : '' }}>대기</option>
<option value="approved" {{ old('status', $record->status) === 'approved' ? 'selected' : '' }}>승인</option>
<option value="rejected" {{ old('status', $record->status) === 'rejected' ? 'selected' : '' }}>반려</option>
<option value="paid" {{ old('status', $record->status) === 'paid' ? 'selected' : '' }}>지급완료</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">설명</label>
<textarea name="description" 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('description', $record->description) }}</textarea>
</div>
<div class="flex justify-end gap-3 pt-4 border-t">
<a href="{{ route('sales.records.index') }}"
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition">
취소
</a>
<button type="submit"
class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
수정
</button>
</div>
</form>
</div>
@endsection

View File

@@ -0,0 +1,174 @@
@extends('layouts.app')
@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>
</div>
<a href="{{ route('sales.records.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="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_count']) }}</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['pending_count']) }}</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['approved_count']) }}</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['total_amount']) }}</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_commission']) }}</div>
</div>
<div class="bg-orange-50 rounded-lg shadow-sm p-4">
<div class="text-sm text-orange-600">대기 금액</div>
<div class="text-xl font-bold text-orange-800">{{ number_format($stats['pending_amount']) }}</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['approved_amount']) }}</div>
</div>
</div>
<!-- 필터 영역 -->
<div class="flex-shrink-0 mb-4">
<form method="GET" class="flex flex-wrap gap-2 sm:gap-4 items-center bg-white p-4 rounded-lg shadow-sm">
<div class="flex-1 min-w-0 w-full sm:w-auto">
<input type="text"
name="search"
value="{{ request('search') }}"
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-36">
<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="pending" {{ request('status') === 'pending' ? 'selected' : '' }}>대기</option>
<option value="approved" {{ request('status') === 'approved' ? 'selected' : '' }}>승인</option>
<option value="rejected" {{ request('status') === 'rejected' ? 'selected' : '' }}>반려</option>
<option value="paid" {{ request('status') === 'paid' ? '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
</select>
</div>
<div class="w-full sm:w-auto flex gap-2">
<input type="date" name="start_date" value="{{ request('start_date') }}"
class="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<span class="self-center text-gray-500">~</span>
<input type="date" name="end_date" value="{{ request('end_date') }}"
class="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</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">
검색
</button>
</form>
</div>
<!-- 테이블 -->
<div class="bg-white rounded-lg shadow-sm overflow-hidden flex-1 flex flex-col min-h-0">
<div class="overflow-x-auto">
<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-right 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-center 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>
<tbody class="bg-white divide-y divide-gray-200">
@forelse($records as $record)
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ $record->record_date->format('Y-m-d') }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">{{ $record->manager?->name ?? '-' }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ $record->record_type }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
@if($record->prospect)
<a href="{{ route('sales.prospects.show', $record->prospect->id) }}" class="text-blue-600 hover:underline">
{{ $record->prospect->company_name }}
</a>
@else
<span class="text-gray-400">-</span>
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium text-gray-900">
{{ number_format($record->amount) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm text-gray-900">
{{ number_format($record->commission) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-center">
<span class="px-2 py-1 text-xs font-medium rounded-full {{ $record->status_color }}">
{{ $record->status_label }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="{{ route('sales.records.show', $record->id) }}" class="text-blue-600 hover:text-blue-900 mr-3">상세</a>
<a href="{{ route('sales.records.edit', $record->id) }}" class="text-indigo-600 hover:text-indigo-900 mr-3">수정</a>
<form action="{{ route('sales.records.destroy', $record->id) }}" method="POST" class="inline"
onsubmit="return confirm('정말 삭제하시겠습니까?')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-900">삭제</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="8" class="px-6 py-12 text-center text-gray-500">
등록된 실적이 없습니다.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- 페이지네이션 -->
@if($records->hasPages())
<div class="px-6 py-4 border-t border-gray-200">
{{ $records->withQueryString()->links() }}
</div>
@endif
</div>
</div>
@endsection

View File

@@ -0,0 +1,91 @@
@extends('layouts.app')
@section('title', '영업실적 상세')
@section('content')
<div class="max-w-2xl mx-auto">
<!-- 페이지 헤더 -->
<div class="mb-6 flex justify-between items-start">
<div>
<a href="{{ route('sales.records.index') }}" 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>
</div>
<a href="{{ route('sales.records.edit', $record->id) }}"
class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transition">
수정
</a>
</div>
<!-- 상세 정보 -->
<div class="bg-white rounded-lg shadow-sm p-6 space-y-6">
<div class="flex items-center justify-between">
<span class="px-3 py-1 text-sm font-medium rounded-full {{ $record->status_color }}">
{{ $record->status_label }}
</span>
<span class="text-gray-500">{{ $record->record_date->format('Y년 m월 d일') }}</span>
</div>
<div class="grid grid-cols-2 gap-6">
<div class="bg-blue-50 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-blue-800">{{ number_format($record->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($record->commission) }}</div>
<div class="text-sm text-green-600">수수료</div>
</div>
</div>
<dl class="space-y-4 border-t pt-6">
<div class="flex justify-between">
<dt class="text-gray-500">실적 유형</dt>
<dd class="font-medium text-gray-900">{{ $record->record_type }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">담당자</dt>
<dd class="font-medium text-gray-900">
@if($record->manager)
<a href="{{ route('sales.managers.show', $record->manager->id) }}" class="text-blue-600 hover:underline">
{{ $record->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($record->prospect)
<a href="{{ route('sales.prospects.show', $record->prospect->id) }}" class="text-blue-600 hover:underline">
{{ $record->prospect->company_name }}
</a>
@else
-
@endif
</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">등록일</dt>
<dd class="font-medium text-gray-900">{{ $record->created_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">{{ $record->updated_at->format('Y-m-d H:i') }}</dd>
</div>
</dl>
@if($record->description)
<div class="border-t pt-6">
<h3 class="text-sm font-medium text-gray-700 mb-2">설명</h3>
<p class="text-gray-900 whitespace-pre-line">{{ $record->description }}</p>
</div>
@endif
</div>
</div>
@endsection

View File

@@ -733,3 +733,19 @@
return view('finance.vat');
})->name('vat');
});
/*
|--------------------------------------------------------------------------
| Sales Management Routes (영업관리)
|--------------------------------------------------------------------------
*/
Route::middleware(['auth', 'hq.member'])->prefix('sales')->name('sales.')->group(function () {
// 영업 담당자 관리
Route::resource('managers', \App\Http\Controllers\Sales\SalesManagerController::class);
// 가망고객 관리
Route::resource('prospects', \App\Http\Controllers\Sales\SalesProspectController::class);
// 영업 실적 관리
Route::resource('records', \App\Http\Controllers\Sales\SalesRecordController::class);
});