Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
43
CLAUDE.md
43
CLAUDE.md
@@ -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가 필요한 페이지
|
||||
|
||||
155
app/Http/Controllers/Sales/SalesManagerController.php
Normal file
155
app/Http/Controllers/Sales/SalesManagerController.php
Normal 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', '담당자가 비활성화되었습니다.');
|
||||
}
|
||||
}
|
||||
170
app/Http/Controllers/Sales/SalesProspectController.php
Normal file
170
app/Http/Controllers/Sales/SalesProspectController.php
Normal 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', '가망고객이 삭제되었습니다.');
|
||||
}
|
||||
}
|
||||
166
app/Http/Controllers/Sales/SalesRecordController.php
Normal file
166
app/Http/Controllers/Sales/SalesRecordController.php
Normal 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', '실적이 삭제되었습니다.');
|
||||
}
|
||||
}
|
||||
120
app/Models/Sales/SalesManager.php
Normal file
120
app/Models/Sales/SalesManager.php
Normal 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);
|
||||
}
|
||||
}
|
||||
139
app/Models/Sales/SalesProspect.php
Normal file
139
app/Models/Sales/SalesProspect.php
Normal 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');
|
||||
}
|
||||
}
|
||||
91
app/Models/Sales/SalesProspectConsultation.php
Normal file
91
app/Models/Sales/SalesProspectConsultation.php
Normal 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);
|
||||
}
|
||||
}
|
||||
97
app/Models/Sales/SalesProspectProduct.php
Normal file
97
app/Models/Sales/SalesProspectProduct.php
Normal 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';
|
||||
}
|
||||
}
|
||||
68
app/Models/Sales/SalesProspectScenario.php
Normal file
68
app/Models/Sales/SalesProspectScenario.php
Normal 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);
|
||||
}
|
||||
}
|
||||
117
app/Models/Sales/SalesRecord.php
Normal file
117
app/Models/Sales/SalesRecord.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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'],
|
||||
]);
|
||||
|
||||
// ========================================
|
||||
// 시스템 그룹
|
||||
// ========================================
|
||||
|
||||
153
resources/views/sales/managers/create.blade.php
Normal file
153
resources/views/sales/managers/create.blade.php
Normal 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
|
||||
108
resources/views/sales/managers/edit.blade.php
Normal file
108
resources/views/sales/managers/edit.blade.php
Normal 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
|
||||
137
resources/views/sales/managers/index.blade.php
Normal file
137
resources/views/sales/managers/index.blade.php
Normal 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
|
||||
128
resources/views/sales/managers/show.blade.php
Normal file
128
resources/views/sales/managers/show.blade.php
Normal 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
|
||||
121
resources/views/sales/prospects/create.blade.php
Normal file
121
resources/views/sales/prospects/create.blade.php
Normal 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
|
||||
106
resources/views/sales/prospects/edit.blade.php
Normal file
106
resources/views/sales/prospects/edit.blade.php
Normal 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
|
||||
172
resources/views/sales/prospects/index.blade.php
Normal file
172
resources/views/sales/prospects/index.blade.php
Normal 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
|
||||
173
resources/views/sales/prospects/show.blade.php
Normal file
173
resources/views/sales/prospects/show.blade.php
Normal 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
|
||||
119
resources/views/sales/records/create.blade.php
Normal file
119
resources/views/sales/records/create.blade.php
Normal 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
|
||||
120
resources/views/sales/records/edit.blade.php
Normal file
120
resources/views/sales/records/edit.blade.php
Normal 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
|
||||
174
resources/views/sales/records/index.blade.php
Normal file
174
resources/views/sales/records/index.blade.php
Normal 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
|
||||
91
resources/views/sales/records/show.blade.php
Normal file
91
resources/views/sales/records/show.blade.php
Normal 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
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user