feat: 테넌트 정보 모달 팝업 기능 추가
- 테넌트 row 클릭 시 모달 팝업 표시 - 컨텍스트 메뉴 (우클릭) 지원 - 탭 구조: 구독정보, 사용자, 부서, 역할, 메뉴 - 메뉴 탭 트리 구조 접기/펼치기 기능 - 삭제된 테넌트 경고 배너 (삭제일, 삭제자 표시) - 복원 버튼으로 즉시 복원 및 모달 새로고침 - 액션 버튼 (수정/삭제) 클릭 시 모달 미표시
This commit is contained in:
@@ -205,4 +205,111 @@ public function stats(Request $request): JsonResponse
|
||||
'data' => $stats,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 모달 정보 조회
|
||||
*/
|
||||
public function modal(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$tenant = $this->tenantService->getTenantForModal($id);
|
||||
|
||||
if (! $tenant) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '테넌트를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$html = view('tenants.partials.modal-info', compact('tenant'))->render();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'html' => $html,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 사용자 탭 데이터
|
||||
*/
|
||||
public function users(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$users = $this->tenantService->getTenantUsers($id);
|
||||
|
||||
$html = view('tenants.partials.modal-users', compact('users'))->render();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'html' => $html,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 부서 탭 데이터
|
||||
*/
|
||||
public function departments(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$departments = $this->tenantService->getTenantDepartments($id);
|
||||
|
||||
$html = view('tenants.partials.modal-departments', compact('departments'))->render();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'html' => $html,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 역할 탭 데이터
|
||||
*/
|
||||
public function roles(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$roles = $this->tenantService->getTenantRoles($id);
|
||||
|
||||
$html = view('tenants.partials.modal-roles', compact('roles'))->render();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'html' => $html,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 메뉴 탭 데이터
|
||||
*/
|
||||
public function menus(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$menus = $this->tenantService->getTenantMenus($id);
|
||||
|
||||
$html = view('tenants.partials.modal-menus', compact('menus'))->render();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'html' => $html,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 구독 정보 탭 데이터
|
||||
*/
|
||||
public function subscription(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$tenant = $this->tenantService->getTenantForModal($id);
|
||||
|
||||
if (! $tenant) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '테넌트를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$subscription = $this->tenantService->getTenantSubscription($id);
|
||||
$usage = $this->tenantService->getTenantUsage($id);
|
||||
|
||||
$html = view('tenants.partials.modal-subscription', compact('tenant', 'subscription', 'usage'))->render();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'html' => $html,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ class Tenant extends Model
|
||||
'fax',
|
||||
// 구독 정보
|
||||
'tenant_st_code',
|
||||
'tenant_type',
|
||||
'template_id',
|
||||
'billing_tp_code',
|
||||
'max_users',
|
||||
'trial_ends_at',
|
||||
@@ -36,6 +38,8 @@ class Tenant extends Model
|
||||
'last_paid_at',
|
||||
// 관리 메모
|
||||
'admin_memo',
|
||||
// 삭제 정보
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -88,6 +92,14 @@ public function roles(): HasMany
|
||||
return $this->hasMany(\App\Models\Permissions\Role::class, 'tenant_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계: 삭제한 사용자
|
||||
*/
|
||||
public function deletedByUser(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'deleted_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 배지 색상 (Blade 뷰에서 사용)
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
use App\Models\Tenants\Tenant;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TenantService
|
||||
{
|
||||
@@ -88,6 +89,10 @@ public function deleteTenant(int $id): bool
|
||||
{
|
||||
$tenant = Tenant::findOrFail($id);
|
||||
|
||||
// 삭제자 기록
|
||||
$tenant->deleted_by = auth()->id();
|
||||
$tenant->save();
|
||||
|
||||
return $tenant->delete();
|
||||
}
|
||||
|
||||
@@ -98,6 +103,9 @@ public function restoreTenant(int $id): bool
|
||||
{
|
||||
$tenant = Tenant::onlyTrashed()->findOrFail($id);
|
||||
|
||||
// 삭제 정보 초기화
|
||||
$tenant->deleted_by = null;
|
||||
|
||||
return $tenant->restore();
|
||||
}
|
||||
|
||||
@@ -156,4 +164,195 @@ public function getTenantStats(): array
|
||||
'trashed' => Tenant::onlyTrashed()->count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 모달용 테넌트 상세 정보 조회
|
||||
*/
|
||||
public function getTenantForModal(int $id): ?Tenant
|
||||
{
|
||||
return Tenant::query()
|
||||
->with('deletedByUser')
|
||||
->withCount(['users', 'departments', 'menus', 'roles'])
|
||||
->withTrashed()
|
||||
->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 소속 사용자 목록 조회
|
||||
*/
|
||||
public function getTenantUsers(int $tenantId): Collection
|
||||
{
|
||||
$tenant = Tenant::find($tenantId);
|
||||
if (! $tenant) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
// 사용자와 함께 역할/부서 정보를 가져옴
|
||||
return $tenant->users()
|
||||
->with(['userRoles' => function ($query) use ($tenantId) {
|
||||
$query->where('tenant_id', $tenantId)->with('role');
|
||||
}, 'departmentUsers' => function ($query) use ($tenantId) {
|
||||
$query->where('tenant_id', $tenantId)->with('department');
|
||||
}])
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->map(function ($user) {
|
||||
// 역할과 부서를 플랫하게 매핑
|
||||
$user->roles = $user->userRoles->pluck('role')->filter();
|
||||
$user->department = $user->departmentUsers->first()?->department;
|
||||
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 부서 목록 조회 (계층 구조)
|
||||
*/
|
||||
public function getTenantDepartments(int $tenantId): Collection
|
||||
{
|
||||
// Department 모델의 users() 관계가 잘못된 네임스페이스를 참조하므로 직접 카운트
|
||||
$departments = \App\Models\Department::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
// 부서별 사용자 수 직접 카운트
|
||||
$userCounts = DB::table('department_user')
|
||||
->whereIn('department_id', $departments->pluck('id'))
|
||||
->selectRaw('department_id, count(*) as cnt')
|
||||
->groupBy('department_id')
|
||||
->pluck('cnt', 'department_id');
|
||||
|
||||
return $departments->map(function ($dept) use ($userCounts, $departments) {
|
||||
// 사용자 수 설정
|
||||
$dept->users_count = $userCounts[$dept->id] ?? 0;
|
||||
|
||||
// 계층 깊이 계산
|
||||
$depth = 0;
|
||||
$parentId = $dept->parent_id;
|
||||
while ($parentId) {
|
||||
$depth++;
|
||||
$parent = $departments->firstWhere('id', $parentId);
|
||||
$parentId = $parent?->parent_id;
|
||||
}
|
||||
$dept->depth = $depth;
|
||||
|
||||
// 상위 부서 이름
|
||||
$dept->parent = $departments->firstWhere('id', $dept->parent_id);
|
||||
|
||||
return $dept;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 역할 목록 조회
|
||||
*/
|
||||
public function getTenantRoles(int $tenantId): Collection
|
||||
{
|
||||
// Role 모델의 userRoles() 관계가 잘못된 네임스페이스를 참조하므로 직접 카운트
|
||||
$roles = \App\Models\Role::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->withCount('permissions')
|
||||
->with('permissions')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
// 역할별 사용자 수 직접 카운트
|
||||
$userCounts = DB::table('user_roles')
|
||||
->whereIn('role_id', $roles->pluck('id'))
|
||||
->selectRaw('role_id, count(*) as cnt')
|
||||
->groupBy('role_id')
|
||||
->pluck('cnt', 'role_id');
|
||||
|
||||
return $roles->map(function ($role) use ($userCounts) {
|
||||
$role->users_count = $userCounts[$role->id] ?? 0;
|
||||
|
||||
return $role;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 메뉴 목록 조회 (계층 구조 - 트리 정렬)
|
||||
*/
|
||||
public function getTenantMenus(int $tenantId): \Illuminate\Support\Collection
|
||||
{
|
||||
$menus = \App\Models\Commons\Menu::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
// 트리 구조로 정렬 후 플랫한 배열로 변환
|
||||
return $this->flattenMenuTree($menus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 트리 구조를 플랫한 배열로 변환 (depth 정보 포함)
|
||||
*/
|
||||
private function flattenMenuTree(Collection $menus, ?int $parentId = null, int $depth = 0): \Illuminate\Support\Collection
|
||||
{
|
||||
$result = collect();
|
||||
|
||||
$filteredMenus = $menus->where('parent_id', $parentId)->sortBy('sort_order');
|
||||
|
||||
foreach ($filteredMenus as $menu) {
|
||||
$menu->depth = $depth;
|
||||
|
||||
// 자식 메뉴 존재 여부 확인
|
||||
$menu->has_children = $menus->where('parent_id', $menu->id)->count() > 0;
|
||||
|
||||
$result->push($menu);
|
||||
|
||||
// 자식 메뉴 재귀적으로 추가
|
||||
$children = $this->flattenMenuTree($menus, $menu->id, $depth + 1);
|
||||
$result = $result->merge($children);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 구독 정보 조회
|
||||
*/
|
||||
public function getTenantSubscription(int $tenantId): object
|
||||
{
|
||||
$tenant = Tenant::find($tenantId);
|
||||
|
||||
// 기본 구독 정보 (실제 구현 시 별도 테이블에서 조회)
|
||||
return (object) [
|
||||
'plan_name' => $tenant?->subscription_plan ?? '기본 플랜',
|
||||
'status' => $tenant?->subscription_status ?? 'active',
|
||||
'started_at' => $tenant?->created_at?->format('Y-m-d'),
|
||||
'expires_at' => $tenant?->subscription_expires_at ?? null,
|
||||
'next_billing_date' => null,
|
||||
'max_users' => $tenant?->max_users ?? null,
|
||||
'max_storage' => $tenant?->max_storage ?? null,
|
||||
'max_storage_mb' => null,
|
||||
'has_api_access' => true,
|
||||
'has_advanced_reports' => false,
|
||||
'features' => [
|
||||
['name' => '기본 기능', 'enabled' => true],
|
||||
['name' => '사용자 관리', 'enabled' => true],
|
||||
['name' => '부서 관리', 'enabled' => true],
|
||||
['name' => '역할 관리', 'enabled' => true],
|
||||
['name' => 'API 접근', 'enabled' => true],
|
||||
['name' => '고급 보고서', 'enabled' => false],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 사용량 정보 조회
|
||||
*/
|
||||
public function getTenantUsage(int $tenantId): object
|
||||
{
|
||||
$tenant = Tenant::withCount('users')->find($tenantId);
|
||||
|
||||
return (object) [
|
||||
'users_count' => $tenant?->users_count ?? 0,
|
||||
'storage_used' => '0 MB',
|
||||
'storage_used_mb' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
139
public/js/context-menu.js
Normal file
139
public/js/context-menu.js
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 컨텍스트 메뉴 (우클릭 메뉴)
|
||||
* 테넌트명, 사용자명 등에서 우클릭 시 해당 위치에 메뉴 표시
|
||||
*/
|
||||
|
||||
class ContextMenu {
|
||||
constructor() {
|
||||
this.menuElement = null;
|
||||
this.currentTarget = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 컨텍스트 메뉴 요소 찾기
|
||||
this.menuElement = document.getElementById('context-menu');
|
||||
if (!this.menuElement) return;
|
||||
|
||||
// 우클릭 이벤트 등록 (이벤트 위임)
|
||||
document.addEventListener('contextmenu', (e) => this.handleContextMenu(e));
|
||||
|
||||
// 클릭 시 메뉴 닫기
|
||||
document.addEventListener('click', () => this.hide());
|
||||
|
||||
// ESC 키로 메뉴 닫기
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') this.hide();
|
||||
});
|
||||
|
||||
// 스크롤 시 메뉴 닫기
|
||||
document.addEventListener('scroll', () => this.hide(), true);
|
||||
}
|
||||
|
||||
handleContextMenu(e) {
|
||||
const trigger = e.target.closest('[data-context-menu]');
|
||||
if (!trigger) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const menuType = trigger.dataset.contextMenu;
|
||||
const entityId = trigger.dataset.entityId;
|
||||
const entityName = trigger.dataset.entityName;
|
||||
|
||||
this.currentTarget = {
|
||||
type: menuType,
|
||||
id: entityId,
|
||||
name: entityName,
|
||||
element: trigger
|
||||
};
|
||||
|
||||
this.show(e.clientX, e.clientY, menuType);
|
||||
}
|
||||
|
||||
show(x, y, menuType) {
|
||||
if (!this.menuElement) return;
|
||||
|
||||
// 메뉴 타입에 따라 항목 표시/숨김
|
||||
this.updateMenuItems(menuType);
|
||||
|
||||
// 메뉴 표시
|
||||
this.menuElement.classList.remove('hidden');
|
||||
|
||||
// 위치 조정 (화면 밖으로 나가지 않도록)
|
||||
const menuRect = this.menuElement.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
let posX = x;
|
||||
let posY = y;
|
||||
|
||||
if (x + menuRect.width > viewportWidth) {
|
||||
posX = viewportWidth - menuRect.width - 10;
|
||||
}
|
||||
|
||||
if (y + menuRect.height > viewportHeight) {
|
||||
posY = viewportHeight - menuRect.height - 10;
|
||||
}
|
||||
|
||||
this.menuElement.style.left = `${posX}px`;
|
||||
this.menuElement.style.top = `${posY}px`;
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (this.menuElement) {
|
||||
this.menuElement.classList.add('hidden');
|
||||
}
|
||||
this.currentTarget = null;
|
||||
}
|
||||
|
||||
updateMenuItems(menuType) {
|
||||
// 모든 메뉴 항목 숨기기
|
||||
const allItems = this.menuElement.querySelectorAll('[data-menu-for]');
|
||||
allItems.forEach(item => {
|
||||
const forTypes = item.dataset.menuFor.split(',');
|
||||
if (forTypes.includes(menuType) || forTypes.includes('all')) {
|
||||
item.classList.remove('hidden');
|
||||
} else {
|
||||
item.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 메뉴 항목 클릭 핸들러
|
||||
handleMenuAction(action) {
|
||||
if (!this.currentTarget) return;
|
||||
|
||||
const { type, id, name } = this.currentTarget;
|
||||
|
||||
switch (action) {
|
||||
case 'view-tenant':
|
||||
if (typeof TenantModal !== 'undefined') {
|
||||
TenantModal.open(id);
|
||||
}
|
||||
break;
|
||||
case 'edit-tenant':
|
||||
window.location.href = `/tenants/${id}/edit`;
|
||||
break;
|
||||
case 'view-user':
|
||||
if (typeof UserModal !== 'undefined') {
|
||||
UserModal.open(id);
|
||||
}
|
||||
break;
|
||||
case 'edit-user':
|
||||
window.location.href = `/users/${id}/edit`;
|
||||
break;
|
||||
default:
|
||||
console.log('Unknown action:', action, { type, id, name });
|
||||
}
|
||||
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
const contextMenu = new ContextMenu();
|
||||
|
||||
// 전역 함수로 노출 (onclick에서 사용)
|
||||
function handleContextMenuAction(action) {
|
||||
contextMenu.handleMenuAction(action);
|
||||
}
|
||||
242
public/js/tenant-modal.js
Normal file
242
public/js/tenant-modal.js
Normal file
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* 테넌트 정보 모달
|
||||
* 테넌트 상세 정보를 팝업으로 표시하고 탭으로 관련 정보 관리
|
||||
*/
|
||||
|
||||
const TenantModal = {
|
||||
modalElement: null,
|
||||
currentTenantId: null,
|
||||
currentTab: 'info',
|
||||
|
||||
init() {
|
||||
this.modalElement = document.getElementById('tenant-modal');
|
||||
if (!this.modalElement) return;
|
||||
|
||||
// ESC 키로 모달 닫기
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && this.isOpen()) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
// 배경 클릭 시 모달 닫기
|
||||
this.modalElement.addEventListener('click', (e) => {
|
||||
if (e.target === this.modalElement) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
isOpen() {
|
||||
return this.modalElement && !this.modalElement.classList.contains('hidden');
|
||||
},
|
||||
|
||||
async open(tenantId) {
|
||||
if (!this.modalElement) {
|
||||
console.error('Tenant modal element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentTenantId = tenantId;
|
||||
this.currentTab = 'subscription';
|
||||
|
||||
// 모달 표시
|
||||
this.modalElement.classList.remove('hidden');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// 로딩 표시
|
||||
this.showLoading();
|
||||
|
||||
// 테넌트 정보 로드
|
||||
await this.loadTenantInfo();
|
||||
},
|
||||
|
||||
close() {
|
||||
if (this.modalElement) {
|
||||
this.modalElement.classList.add('hidden');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
this.currentTenantId = null;
|
||||
},
|
||||
|
||||
showLoading() {
|
||||
const content = document.getElementById('tenant-modal-content');
|
||||
if (content) {
|
||||
content.innerHTML = `
|
||||
<div class="flex items-center justify-center h-64">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
|
||||
async loadTenantInfo() {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/tenants/${this.currentTenantId}/modal`, {
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.html) {
|
||||
const content = document.getElementById('tenant-modal-content');
|
||||
if (content) {
|
||||
content.innerHTML = data.html;
|
||||
}
|
||||
// 구독정보 탭 자동 로드
|
||||
this.currentTab = ''; // switchTab에서 early return 방지
|
||||
await this.switchTab('subscription');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load tenant info:', error);
|
||||
this.showError('테넌트 정보를 불러오는데 실패했습니다.');
|
||||
}
|
||||
},
|
||||
|
||||
async switchTab(tab) {
|
||||
if (this.currentTab === tab) return;
|
||||
|
||||
this.currentTab = tab;
|
||||
|
||||
// 탭 버튼 스타일 업데이트
|
||||
document.querySelectorAll('#tenant-modal [data-tab-btn]').forEach(btn => {
|
||||
if (btn.dataset.tabBtn === tab) {
|
||||
btn.classList.add('border-blue-500', 'text-blue-600');
|
||||
btn.classList.remove('border-transparent', 'text-gray-500');
|
||||
} else {
|
||||
btn.classList.remove('border-blue-500', 'text-blue-600');
|
||||
btn.classList.add('border-transparent', 'text-gray-500');
|
||||
}
|
||||
});
|
||||
|
||||
// 탭 콘텐츠 로드
|
||||
const tabContent = document.getElementById('tenant-modal-tab-content');
|
||||
if (!tabContent) return;
|
||||
|
||||
// 로딩 표시
|
||||
tabContent.innerHTML = `
|
||||
<div class="flex items-center justify-center h-32">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/tenants/${this.currentTenantId}/${tab}`, {
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.html) {
|
||||
tabContent.innerHTML = data.html;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load ${tab} tab:`, error);
|
||||
tabContent.innerHTML = `
|
||||
<div class="text-center py-8 text-red-500">
|
||||
데이터를 불러오는데 실패했습니다.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
|
||||
showError(message) {
|
||||
const content = document.getElementById('tenant-modal-content');
|
||||
if (content) {
|
||||
content.innerHTML = `
|
||||
<div class="flex flex-col items-center justify-center h-64 text-red-500">
|
||||
<svg class="w-12 h-12 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<p>${message}</p>
|
||||
<button onclick="TenantModal.close()" class="mt-4 px-4 py-2 bg-gray-200 rounded hover:bg-gray-300">
|
||||
닫기
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
|
||||
// 수정 페이지로 이동
|
||||
goToEdit() {
|
||||
if (this.currentTenantId) {
|
||||
window.location.href = `/tenants/${this.currentTenantId}/edit`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 모달 내 메뉴 자식 토글
|
||||
* @param {number} menuId 부모 메뉴 ID
|
||||
*/
|
||||
window.toggleModalMenuChildren = function(menuId) {
|
||||
const rows = document.querySelectorAll('.modal-menu-row');
|
||||
const toggleBtn = document.querySelector(`.modal-menu-toggle[data-menu-id="${menuId}"]`);
|
||||
const chevron = toggleBtn?.querySelector('.chevron-icon');
|
||||
|
||||
// 직접 자식들 찾기
|
||||
const directChildren = [];
|
||||
rows.forEach(row => {
|
||||
if (row.dataset.parentId == menuId) {
|
||||
directChildren.push(row);
|
||||
}
|
||||
});
|
||||
|
||||
if (directChildren.length === 0) return;
|
||||
|
||||
// 첫 번째 자식의 현재 상태 확인
|
||||
const isHidden = directChildren[0].classList.contains('hidden');
|
||||
|
||||
if (isHidden) {
|
||||
// 펼치기: 직접 자식만 표시
|
||||
directChildren.forEach(child => {
|
||||
child.classList.remove('hidden');
|
||||
});
|
||||
// 쉐브론 회전 초기화
|
||||
if (chevron) {
|
||||
chevron.classList.remove('-rotate-90');
|
||||
}
|
||||
} else {
|
||||
// 접기: 모든 자손 숨기기 (재귀적으로)
|
||||
window.hideModalMenuDescendants(menuId, rows);
|
||||
// 쉐브론 회전
|
||||
if (chevron) {
|
||||
chevron.classList.add('-rotate-90');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 특정 메뉴의 모든 자손 숨기기 (재귀)
|
||||
* @param {number} parentId 부모 메뉴 ID
|
||||
* @param {NodeList} rows 모든 메뉴 행
|
||||
*/
|
||||
window.hideModalMenuDescendants = function(parentId, rows) {
|
||||
rows.forEach(row => {
|
||||
if (row.dataset.parentId == parentId) {
|
||||
row.classList.add('hidden');
|
||||
// 자식의 쉐브론도 접힌 상태로
|
||||
const childToggle = row.querySelector('.modal-menu-toggle');
|
||||
const childChevron = childToggle?.querySelector('.chevron-icon');
|
||||
if (childChevron) {
|
||||
childChevron.classList.add('-rotate-90');
|
||||
}
|
||||
// 손자들도 재귀적으로 숨기기
|
||||
const childMenuId = row.dataset.menuId;
|
||||
if (childMenuId) {
|
||||
window.hideModalMenuDescendants(childMenuId, rows);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// DOM 로드 시 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
TenantModal.init();
|
||||
});
|
||||
52
resources/views/components/context-menu.blade.php
Normal file
52
resources/views/components/context-menu.blade.php
Normal file
@@ -0,0 +1,52 @@
|
||||
{{-- 컨텍스트 메뉴 (우클릭 메뉴) --}}
|
||||
<div id="context-menu"
|
||||
class="hidden fixed z-50 bg-white rounded-lg shadow-lg border border-gray-200 py-1 min-w-[160px]"
|
||||
style="left: 0; top: 0;">
|
||||
|
||||
{{-- 테넌트 관련 메뉴 --}}
|
||||
<button type="button"
|
||||
data-menu-for="tenant"
|
||||
onclick="handleContextMenuAction('view-tenant')"
|
||||
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2">
|
||||
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
테넌트 조회
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
data-menu-for="tenant"
|
||||
onclick="handleContextMenuAction('edit-tenant')"
|
||||
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2">
|
||||
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
테넌트 수정
|
||||
</button>
|
||||
|
||||
{{-- 구분선 --}}
|
||||
<div data-menu-for="tenant" class="border-t border-gray-100 my-1"></div>
|
||||
|
||||
{{-- 사용자 관련 메뉴 --}}
|
||||
<button type="button"
|
||||
data-menu-for="user"
|
||||
onclick="handleContextMenuAction('view-user')"
|
||||
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2">
|
||||
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
사용자 조회
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
data-menu-for="user"
|
||||
onclick="handleContextMenuAction('edit-user')"
|
||||
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2">
|
||||
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
사용자 수정
|
||||
</button>
|
||||
</div>
|
||||
39
resources/views/components/tenant-modal.blade.php
Normal file
39
resources/views/components/tenant-modal.blade.php
Normal file
@@ -0,0 +1,39 @@
|
||||
{{-- 테넌트 정보 모달 --}}
|
||||
<div id="tenant-modal"
|
||||
class="hidden fixed inset-0 z-50 overflow-y-auto"
|
||||
aria-labelledby="tenant-modal-title"
|
||||
role="dialog"
|
||||
aria-modal="true">
|
||||
|
||||
{{-- 배경 오버레이 --}}
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
|
||||
|
||||
{{-- 모달 컨테이너 --}}
|
||||
<div class="flex min-h-full items-start justify-center p-4 pt-16">
|
||||
<div class="relative bg-white rounded-lg shadow-xl w-full max-w-5xl max-h-[85vh] flex flex-col">
|
||||
|
||||
{{-- 모달 헤더 --}}
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 flex-shrink-0">
|
||||
<h2 id="tenant-modal-title" class="text-xl font-semibold text-gray-800">
|
||||
테넌트 정보
|
||||
</h2>
|
||||
<button type="button"
|
||||
onclick="TenantModal.close()"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- 모달 콘텐츠 --}}
|
||||
<div id="tenant-modal-content" class="flex-1 overflow-y-auto">
|
||||
{{-- 콘텐츠는 JS로 로드됨 --}}
|
||||
<div class="flex items-center justify-center h-64">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -26,6 +26,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 전역 컨텍스트 메뉴 -->
|
||||
@include('components.context-menu')
|
||||
|
||||
<!-- 테넌트 정보 모달 -->
|
||||
@include('components.tenant-modal')
|
||||
|
||||
<!-- HTMX CSRF 토큰 설정 -->
|
||||
<script>
|
||||
document.body.addEventListener('htmx:configRequest', (event) => {
|
||||
@@ -34,6 +40,8 @@
|
||||
</script>
|
||||
|
||||
<script src="{{ asset('js/pagination.js') }}"></script>
|
||||
<script src="{{ asset('js/context-menu.js') }}"></script>
|
||||
<script src="{{ asset('js/tenant-modal.js') }}"></script>
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -109,6 +109,10 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
}
|
||||
}).then(() => {
|
||||
htmx.trigger('#tenant-table', 'filterSubmit');
|
||||
// 모달이 열려있으면 내용 새로고침
|
||||
if (TenantModal.isOpen() && TenantModal.currentTenantId === id) {
|
||||
TenantModal.loadTenantInfo();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
74
resources/views/tenants/partials/modal-departments.blade.php
Normal file
74
resources/views/tenants/partials/modal-departments.blade.php
Normal file
@@ -0,0 +1,74 @@
|
||||
{{-- 테넌트 모달 - 부서 탭 --}}
|
||||
<div class="space-y-4">
|
||||
{{-- 헤더 --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-gray-700">부서 목록</h3>
|
||||
<span class="text-xs text-gray-500">총 {{ $departments->count() }}개</span>
|
||||
</div>
|
||||
|
||||
@if($departments->count() > 0)
|
||||
{{-- 부서 트리 구조 --}}
|
||||
<div class="overflow-hidden border border-gray-200 rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">부서명</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">코드</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">상위 부서</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">소속 인원</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($departments as $department)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-2 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
@if($department->depth > 0)
|
||||
<span class="text-gray-300 mr-1">
|
||||
@for($i = 0; $i < $department->depth; $i++)
|
||||
|
||||
@endfor
|
||||
└
|
||||
</span>
|
||||
@endif
|
||||
<span class="text-sm font-medium text-gray-900">
|
||||
{{ $department->name }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $department->code ?? '-' }}
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $department->parent?->name ?? '-' }}
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $department->users_count ?? 0 }}명
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">
|
||||
@if($department->is_active ?? true)
|
||||
<span class="px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-800">
|
||||
활성
|
||||
</span>
|
||||
@else
|
||||
<span class="px-2 py-0.5 text-xs font-semibold rounded-full bg-gray-100 text-gray-800">
|
||||
비활성
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
{{-- 빈 상태 --}}
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<svg class="w-12 h-12 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
<p>등록된 부서가 없습니다.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
159
resources/views/tenants/partials/modal-info.blade.php
Normal file
159
resources/views/tenants/partials/modal-info.blade.php
Normal file
@@ -0,0 +1,159 @@
|
||||
{{-- 테넌트 모달 - 기본 정보 + 탭 구조 --}}
|
||||
<div class="p-6">
|
||||
{{-- 삭제된 테넌트 경고 배너 --}}
|
||||
@if($tenant->deleted_at)
|
||||
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="w-6 h-6 text-red-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-red-800">이 테넌트는 삭제되었습니다.</p>
|
||||
<p class="text-xs text-red-600 mt-0.5">
|
||||
삭제일: {{ $tenant->deleted_at->format('Y-m-d H:i') }}
|
||||
@if($tenant->deletedByUser)
|
||||
· 삭제자: {{ $tenant->deletedByUser->name }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
onclick="event.stopPropagation(); confirmRestore({{ $tenant->id }}, '{{ $tenant->company_name }}')"
|
||||
class="px-3 py-1.5 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors">
|
||||
복원하기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 상단: 기본 정보 카드 --}}
|
||||
<div class="bg-gray-50 rounded-lg p-4 mb-6">
|
||||
<div class="flex gap-4">
|
||||
{{-- 좌측: 아이콘 --}}
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-20 h-20 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-10 h-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 우측: 정보 테이블 --}}
|
||||
<div class="flex-1">
|
||||
<table class="w-full text-sm">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="py-1 pr-4 text-gray-500 w-24">회사명</td>
|
||||
<td class="py-1 font-medium text-gray-900">{{ $tenant->company_name }}</td>
|
||||
<td class="py-1 pr-4 text-gray-500 w-24">코드</td>
|
||||
<td class="py-1 font-medium text-gray-900">{{ $tenant->code }}</td>
|
||||
<td class="py-1 pr-4 text-gray-500 w-24">유형</td>
|
||||
<td class="py-1">
|
||||
@php
|
||||
$typeLabels = ['STD' => '일반', 'TPL' => '템플릿', 'HQ' => '본사'];
|
||||
$typeColors = [
|
||||
'STD' => 'bg-gray-100 text-gray-800',
|
||||
'TPL' => 'bg-purple-100 text-purple-800',
|
||||
'HQ' => 'bg-blue-100 text-blue-800'
|
||||
];
|
||||
$type = $tenant->tenant_type ?? 'STD';
|
||||
@endphp
|
||||
<span class="px-2 py-0.5 text-xs font-semibold rounded-full {{ $typeColors[$type] ?? 'bg-gray-100 text-gray-800' }}">
|
||||
{{ $typeLabels[$type] ?? $type }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-1 pr-4 text-gray-500">대표자</td>
|
||||
<td class="py-1 text-gray-900">{{ $tenant->ceo_name ?? '-' }}</td>
|
||||
<td class="py-1 pr-4 text-gray-500">사업자번호</td>
|
||||
<td class="py-1 text-gray-900">{{ $tenant->business_num ?? '-' }}</td>
|
||||
<td class="py-1 pr-4 text-gray-500">상태</td>
|
||||
<td class="py-1">
|
||||
<span class="px-2 py-0.5 text-xs font-semibold rounded-full
|
||||
{{ $tenant->status_badge_color === 'success' ? 'bg-green-100 text-green-800' : '' }}
|
||||
{{ $tenant->status_badge_color === 'warning' ? 'bg-yellow-100 text-yellow-800' : '' }}
|
||||
{{ $tenant->status_badge_color === 'error' ? 'bg-red-100 text-red-800' : '' }}">
|
||||
{{ $tenant->status_label }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-1 pr-4 text-gray-500">이메일</td>
|
||||
<td class="py-1 text-gray-900">{{ $tenant->email ?? '-' }}</td>
|
||||
<td class="py-1 pr-4 text-gray-500">전화번호</td>
|
||||
<td class="py-1 text-gray-900">{{ $tenant->phone ?? '-' }}</td>
|
||||
<td class="py-1 pr-4 text-gray-500">등록일</td>
|
||||
<td class="py-1 text-gray-900">{{ $tenant->created_at?->format('Y-m-d') ?? '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-1 pr-4 text-gray-500">주소</td>
|
||||
<td colspan="5" class="py-1 text-gray-900">{{ $tenant->address ?? '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 탭 메뉴 --}}
|
||||
<div class="border-b border-gray-200 mb-4">
|
||||
<nav class="flex -mb-px gap-1">
|
||||
<button type="button"
|
||||
data-tab-btn="users"
|
||||
onclick="TenantModal.switchTab('users')"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
||||
사용자 <span class="ml-1 px-1.5 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">{{ $tenant->users_count ?? 0 }}</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
data-tab-btn="departments"
|
||||
onclick="TenantModal.switchTab('departments')"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
||||
부서 <span class="ml-1 px-1.5 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">{{ $tenant->departments_count ?? 0 }}</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
data-tab-btn="roles"
|
||||
onclick="TenantModal.switchTab('roles')"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
||||
역할 <span class="ml-1 px-1.5 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">{{ $tenant->roles_count ?? 0 }}</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
data-tab-btn="menus"
|
||||
onclick="TenantModal.switchTab('menus')"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
||||
메뉴 <span class="ml-1 px-1.5 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">{{ $tenant->menus_count ?? 0 }}</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
data-tab-btn="subscription"
|
||||
onclick="TenantModal.switchTab('subscription')"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
||||
구독 정보
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{{-- 탭 콘텐츠 --}}
|
||||
<div id="tenant-modal-tab-content" class="min-h-[200px]">
|
||||
<div class="text-center py-12 text-gray-500">
|
||||
<svg class="w-12 h-12 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<p>위 탭을 선택하여 상세 정보를 확인하세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 하단 버튼 --}}
|
||||
<div class="flex justify-end gap-2 mt-6 pt-4 border-t border-gray-200">
|
||||
<button type="button"
|
||||
onclick="TenantModal.close()"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
|
||||
닫기
|
||||
</button>
|
||||
<button type="button"
|
||||
onclick="TenantModal.goToEdit()"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">
|
||||
수정
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
108
resources/views/tenants/partials/modal-menus.blade.php
Normal file
108
resources/views/tenants/partials/modal-menus.blade.php
Normal file
@@ -0,0 +1,108 @@
|
||||
{{-- 테넌트 모달 - 메뉴 탭 --}}
|
||||
<div class="space-y-4">
|
||||
{{-- 헤더 --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-gray-700">메뉴 목록</h3>
|
||||
<span class="text-xs text-gray-500">총 {{ $menus->count() }}개</span>
|
||||
</div>
|
||||
|
||||
@if($menus->count() > 0)
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-700 uppercase">ID</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-700 uppercase">메뉴명</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-700 uppercase">URL</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-700 uppercase">정렬</th>
|
||||
<th class="px-4 py-2 text-center text-xs font-semibold text-gray-700 uppercase">활성</th>
|
||||
<th class="px-4 py-2 text-center text-xs font-semibold text-gray-700 uppercase">숨김</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($menus as $menu)
|
||||
<tr class="hover:bg-gray-50 modal-menu-row"
|
||||
data-menu-id="{{ $menu->id }}"
|
||||
data-parent-id="{{ $menu->parent_id ?? '' }}"
|
||||
data-depth="{{ $menu->depth ?? 0 }}">
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ $menu->id }}
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">
|
||||
<div class="flex items-center gap-2" style="padding-left: {{ (($menu->depth ?? 0) * 1.5) }}rem;">
|
||||
{{-- 트리 구조 표시 --}}
|
||||
@if(($menu->depth ?? 0) > 0)
|
||||
<span class="text-gray-300 text-xs font-mono flex-shrink-0">└─</span>
|
||||
@endif
|
||||
|
||||
{{-- 폴더/아이템 아이콘 --}}
|
||||
@if($menu->has_children ?? false)
|
||||
<button type="button"
|
||||
onclick="toggleModalMenuChildren({{ $menu->id }})"
|
||||
class="modal-menu-toggle flex items-center text-blue-500 hover:text-blue-700 focus:outline-none"
|
||||
data-menu-id="{{ $menu->id }}">
|
||||
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<svg class="w-3 h-3 ml-0.5 transform transition-transform chevron-icon" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
@else
|
||||
<svg class="w-4 h-4 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
@endif
|
||||
|
||||
{{-- 메뉴 정보 --}}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm {{ ($menu->depth ?? 0) === 0 ? 'font-semibold text-gray-900' : 'font-medium text-gray-700' }}">
|
||||
{{ $menu->name }}
|
||||
</span>
|
||||
@if($menu->is_external ?? false)
|
||||
<span class="text-xs text-blue-600">(외부)</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">
|
||||
@if(($menu->is_external ?? false) && $menu->external_url)
|
||||
<a href="{{ $menu->external_url }}" target="_blank" class="text-blue-600 hover:underline">
|
||||
{{ Str::limit($menu->external_url, 25) }}
|
||||
</a>
|
||||
@elseif($menu->url)
|
||||
{{ Str::limit($menu->url, 25) }}
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $menu->sort_order ?? 0 }}
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-center">
|
||||
{{-- 활성 상태 표시 (액션 없음) --}}
|
||||
<span class="relative inline-flex h-4 w-8 items-center rounded-full {{ $menu->is_active ? 'bg-blue-500' : 'bg-gray-300' }}">
|
||||
<span class="inline-block h-3 w-3 transform rounded-full bg-white shadow-sm {{ $menu->is_active ? 'translate-x-4' : 'translate-x-0.5' }}"></span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-center">
|
||||
{{-- 숨김 상태 표시 (액션 없음) --}}
|
||||
<span class="relative inline-flex h-4 w-8 items-center rounded-full {{ $menu->hidden ? 'bg-amber-500' : 'bg-gray-300' }}">
|
||||
<span class="inline-block h-3 w-3 transform rounded-full bg-white shadow-sm {{ $menu->hidden ? 'translate-x-4' : 'translate-x-0.5' }}"></span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
{{-- 빈 상태 --}}
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<svg class="w-12 h-12 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<p>등록된 메뉴가 없습니다.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
77
resources/views/tenants/partials/modal-roles.blade.php
Normal file
77
resources/views/tenants/partials/modal-roles.blade.php
Normal file
@@ -0,0 +1,77 @@
|
||||
{{-- 테넌트 모달 - 역할 탭 --}}
|
||||
<div class="space-y-4">
|
||||
{{-- 헤더 --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-gray-700">역할 목록</h3>
|
||||
<span class="text-xs text-gray-500">총 {{ $roles->count() }}개</span>
|
||||
</div>
|
||||
|
||||
@if($roles->count() > 0)
|
||||
{{-- 역할 카드 그리드 --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@foreach($roles as $role)
|
||||
<div class="border border-gray-200 rounded-lg p-4 hover:bg-gray-50">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h4 class="text-sm font-medium text-gray-900">{{ $role->name }}</h4>
|
||||
@if($role->description)
|
||||
<p class="mt-1 text-xs text-gray-500">{{ $role->description }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
@if($role->is_system ?? false)
|
||||
<span class="px-2 py-0.5 text-xs font-semibold rounded-full bg-purple-100 text-purple-800">
|
||||
시스템
|
||||
</span>
|
||||
@else
|
||||
<span class="px-2 py-0.5 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">
|
||||
사용자 정의
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 역할 통계 --}}
|
||||
<div class="mt-3 flex items-center gap-4 text-xs text-gray-500">
|
||||
<span class="flex items-center gap-1">
|
||||
<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="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
{{ $role->users_count ?? 0 }}명
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<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="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
{{ $role->permissions_count ?? 0 }}개 권한
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{-- 주요 권한 미리보기 --}}
|
||||
@if(isset($role->permissions) && $role->permissions->count() > 0)
|
||||
<div class="mt-3 flex flex-wrap gap-1">
|
||||
@foreach($role->permissions->take(3) as $permission)
|
||||
<span class="px-1.5 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">
|
||||
{{ $permission->name }}
|
||||
</span>
|
||||
@endforeach
|
||||
@if($role->permissions->count() > 3)
|
||||
<span class="px-1.5 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">
|
||||
+{{ $role->permissions->count() - 3 }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
{{-- 빈 상태 --}}
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<svg class="w-12 h-12 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
<p>등록된 역할이 없습니다.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
135
resources/views/tenants/partials/modal-subscription.blade.php
Normal file
135
resources/views/tenants/partials/modal-subscription.blade.php
Normal file
@@ -0,0 +1,135 @@
|
||||
{{-- 테넌트 모달 - 구독 정보 탭 --}}
|
||||
<div class="space-y-4">
|
||||
{{-- 헤더 --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-gray-700">구독 정보</h3>
|
||||
</div>
|
||||
|
||||
{{-- 구독 상태 카드 --}}
|
||||
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-4 border border-blue-100">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">현재 플랜</span>
|
||||
<h4 class="text-xl font-bold text-gray-900 mt-1">
|
||||
{{ $subscription->plan_name ?? '기본 플랜' }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
@php
|
||||
$statusColors = [
|
||||
'active' => 'bg-green-100 text-green-800',
|
||||
'trial' => 'bg-blue-100 text-blue-800',
|
||||
'expired' => 'bg-red-100 text-red-800',
|
||||
'cancelled' => 'bg-gray-100 text-gray-800',
|
||||
];
|
||||
$statusLabels = [
|
||||
'active' => '활성',
|
||||
'trial' => '체험판',
|
||||
'expired' => '만료',
|
||||
'cancelled' => '취소됨',
|
||||
];
|
||||
$status = $subscription->status ?? 'active';
|
||||
@endphp
|
||||
<span class="px-3 py-1 text-sm font-semibold rounded-full {{ $statusColors[$status] ?? 'bg-gray-100 text-gray-800' }}">
|
||||
{{ $statusLabels[$status] ?? $status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 구독 상세 정보 --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{{-- 기간 정보 --}}
|
||||
<div class="border border-gray-200 rounded-lg p-4">
|
||||
<h5 class="text-xs font-medium text-gray-500 uppercase mb-3">기간 정보</h5>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-500">시작일</span>
|
||||
<span class="text-gray-900">{{ $subscription->started_at ?? $tenant->created_at?->format('Y-m-d') ?? '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-500">만료일</span>
|
||||
<span class="text-gray-900">{{ $subscription->expires_at ?? '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-500">갱신 예정일</span>
|
||||
<span class="text-gray-900">{{ $subscription->next_billing_date ?? '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 사용량 정보 --}}
|
||||
<div class="border border-gray-200 rounded-lg p-4">
|
||||
<h5 class="text-xs font-medium text-gray-500 uppercase mb-3">사용량</h5>
|
||||
<div class="space-y-3">
|
||||
{{-- 사용자 수 --}}
|
||||
<div>
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span class="text-gray-500">사용자</span>
|
||||
<span class="text-gray-900">
|
||||
{{ $usage->users_count ?? $tenant->users_count ?? 0 }} / {{ $subscription->max_users ?? '무제한' }}
|
||||
</span>
|
||||
</div>
|
||||
@if(isset($subscription->max_users) && $subscription->max_users > 0)
|
||||
@php
|
||||
$usersPercent = min(100, (($usage->users_count ?? $tenant->users_count ?? 0) / $subscription->max_users) * 100);
|
||||
@endphp
|
||||
<div class="w-full bg-gray-200 rounded-full h-1.5">
|
||||
<div class="bg-blue-600 h-1.5 rounded-full" style="width: {{ $usersPercent }}%"></div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- 저장 용량 --}}
|
||||
<div>
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span class="text-gray-500">저장 용량</span>
|
||||
<span class="text-gray-900">
|
||||
{{ $usage->storage_used ?? '0 MB' }} / {{ $subscription->max_storage ?? '무제한' }}
|
||||
</span>
|
||||
</div>
|
||||
@if(isset($subscription->max_storage_mb) && $subscription->max_storage_mb > 0)
|
||||
@php
|
||||
$storagePercent = min(100, (($usage->storage_used_mb ?? 0) / $subscription->max_storage_mb) * 100);
|
||||
@endphp
|
||||
<div class="w-full bg-gray-200 rounded-full h-1.5">
|
||||
<div class="bg-green-600 h-1.5 rounded-full" style="width: {{ $storagePercent }}%"></div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 포함된 기능 --}}
|
||||
<div class="border border-gray-200 rounded-lg p-4">
|
||||
<h5 class="text-xs font-medium text-gray-500 uppercase mb-3">포함된 기능</h5>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||
@php
|
||||
$features = $subscription->features ?? [
|
||||
['name' => '기본 기능', 'enabled' => true],
|
||||
['name' => '사용자 관리', 'enabled' => true],
|
||||
['name' => '부서 관리', 'enabled' => true],
|
||||
['name' => '역할 관리', 'enabled' => true],
|
||||
['name' => 'API 접근', 'enabled' => $subscription->has_api_access ?? false],
|
||||
['name' => '고급 보고서', 'enabled' => $subscription->has_advanced_reports ?? false],
|
||||
];
|
||||
@endphp
|
||||
@foreach($features as $feature)
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
@if($feature['enabled'] ?? false)
|
||||
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span class="text-gray-900">{{ $feature['name'] }}</span>
|
||||
@else
|
||||
<svg class="w-4 h-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<span class="text-gray-400">{{ $feature['name'] }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
86
resources/views/tenants/partials/modal-users.blade.php
Normal file
86
resources/views/tenants/partials/modal-users.blade.php
Normal file
@@ -0,0 +1,86 @@
|
||||
{{-- 테넌트 모달 - 사용자 탭 --}}
|
||||
<div class="space-y-4">
|
||||
{{-- 헤더 --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-gray-700">소속 사용자 목록</h3>
|
||||
<span class="text-xs text-gray-500">총 {{ $users->count() }}명</span>
|
||||
</div>
|
||||
|
||||
@if($users->count() > 0)
|
||||
{{-- 사용자 테이블 --}}
|
||||
<div class="overflow-hidden border border-gray-200 rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">이름</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">이메일</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">부서</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">역할</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">상태</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">가입일</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($users as $user)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-2 whitespace-nowrap">
|
||||
<span class="text-sm font-medium text-gray-900 cursor-pointer hover:text-blue-600"
|
||||
data-context-menu="user"
|
||||
data-entity-id="{{ $user->id }}"
|
||||
data-entity-name="{{ $user->name }}">
|
||||
{{ $user->name }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $user->email }}
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $user->department?->name ?? '-' }}
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">
|
||||
@if($user->roles && $user->roles->count() > 0)
|
||||
<div class="flex flex-wrap gap-1">
|
||||
@foreach($user->roles->take(2) as $role)
|
||||
<span class="px-1.5 py-0.5 text-xs bg-blue-100 text-blue-800 rounded">
|
||||
{{ $role->name }}
|
||||
</span>
|
||||
@endforeach
|
||||
@if($user->roles->count() > 2)
|
||||
<span class="px-1.5 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">
|
||||
+{{ $user->roles->count() - 2 }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<span class="text-xs text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">
|
||||
@if($user->is_active)
|
||||
<span class="px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-800">
|
||||
활성
|
||||
</span>
|
||||
@else
|
||||
<span class="px-2 py-0.5 text-xs font-semibold rounded-full bg-gray-100 text-gray-800">
|
||||
비활성
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $user->created_at?->format('Y-m-d') ?? '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
{{-- 빈 상태 --}}
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<svg class="w-12 h-12 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<p>등록된 사용자가 없습니다.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -6,6 +6,7 @@
|
||||
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">회사명</th>
|
||||
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">코드</th>
|
||||
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">상태</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">유형</th>
|
||||
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">이메일</th>
|
||||
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">전화번호</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">사용자</th>
|
||||
@@ -18,12 +19,19 @@
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@forelse($tenants as $tenant)
|
||||
<tr class="{{ $tenant->deleted_at ? 'bg-gray-100' : '' }}">
|
||||
<tr class="{{ $tenant->deleted_at ? 'bg-gray-100' : '' }} hover:bg-gray-50 cursor-pointer"
|
||||
onclick="TenantModal.open({{ $tenant->id }})"
|
||||
data-tenant-id="{{ $tenant->id }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ $tenant->id }}
|
||||
</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $tenant->company_name }}</div>
|
||||
<div class="text-sm font-medium text-gray-900 cursor-pointer hover:text-blue-600"
|
||||
data-context-menu="tenant"
|
||||
data-entity-id="{{ $tenant->id }}"
|
||||
data-entity-name="{{ $tenant->company_name }}">
|
||||
{{ $tenant->company_name }}
|
||||
</div>
|
||||
@if($tenant->ceo_name)
|
||||
<div class="text-sm text-gray-500">대표: {{ $tenant->ceo_name }}</div>
|
||||
@endif
|
||||
@@ -39,6 +47,20 @@
|
||||
{{ $tenant->status_label }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap text-center">
|
||||
@php
|
||||
$typeLabels = ['STD' => '일반', 'TPL' => '템플릿', 'HQ' => '본사'];
|
||||
$typeColors = [
|
||||
'STD' => 'bg-gray-100 text-gray-800',
|
||||
'TPL' => 'bg-purple-100 text-purple-800',
|
||||
'HQ' => 'bg-blue-100 text-blue-800'
|
||||
];
|
||||
$type = $tenant->tenant_type ?? 'STD';
|
||||
@endphp
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $typeColors[$type] ?? 'bg-gray-100 text-gray-800' }}">
|
||||
{{ $typeLabels[$type] ?? $type }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $tenant->email ?? '-' }}
|
||||
</td>
|
||||
@@ -60,7 +82,7 @@
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $tenant->created_at?->format('Y-m-d') ?? '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium" onclick="event.stopPropagation()">
|
||||
@if($tenant->deleted_at)
|
||||
<!-- 삭제된 항목 -->
|
||||
<button onclick="confirmRestore({{ $tenant->id }}, '{{ $tenant->company_name }}')"
|
||||
@@ -76,6 +98,7 @@ class="text-red-600 hover:text-red-900">
|
||||
@else
|
||||
<!-- 활성 항목 -->
|
||||
<a href="{{ route('tenants.edit', $tenant->id) }}"
|
||||
onclick="event.stopPropagation()"
|
||||
class="text-blue-600 hover:text-blue-900 mr-3">
|
||||
수정
|
||||
</a>
|
||||
@@ -88,7 +111,7 @@ class="text-red-600 hover:text-red-900">
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="12" class="px-6 py-12 text-center text-gray-500">
|
||||
<td colspan="13" class="px-6 py-12 text-center text-gray-500">
|
||||
등록된 테넌트가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -35,6 +35,14 @@
|
||||
// 추가 액션
|
||||
Route::post('/{id}/restore', [TenantController::class, 'restore'])->name('restore');
|
||||
Route::delete('/{id}/force', [TenantController::class, 'forceDestroy'])->name('forceDestroy');
|
||||
|
||||
// 모달 관련 API
|
||||
Route::get('/{id}/modal', [TenantController::class, 'modal'])->name('modal');
|
||||
Route::get('/{id}/users', [TenantController::class, 'users'])->name('users');
|
||||
Route::get('/{id}/departments', [TenantController::class, 'departments'])->name('departments');
|
||||
Route::get('/{id}/roles', [TenantController::class, 'roles'])->name('roles');
|
||||
Route::get('/{id}/menus', [TenantController::class, 'menus'])->name('menus');
|
||||
Route::get('/{id}/subscription', [TenantController::class, 'subscription'])->name('subscription');
|
||||
});
|
||||
|
||||
// 역할 관리 API
|
||||
|
||||
Reference in New Issue
Block a user