294 lines
13 KiB
PHP
294 lines
13 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', '사용량조회')
|
|
|
|
@section('content')
|
|
<!-- 현재 테넌트 정보 카드 -->
|
|
@if($currentTenant)
|
|
<div class="rounded-xl shadow-lg p-5 mb-6" style="background: linear-gradient(to right, #4f46e5, #7c3aed); color: white;">
|
|
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
|
<div class="flex items-center gap-4">
|
|
<div class="p-3 rounded-xl" style="background: rgba(255,255,255,0.2);">
|
|
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<span class="px-2 py-0.5 rounded-full text-xs font-bold" style="background: rgba(255,255,255,0.2);">T-ID: {{ $currentTenant->id }}</span>
|
|
@if($isHeadquarters ?? false)
|
|
<span class="px-2 py-0.5 rounded-full text-xs font-bold" style="background: #facc15; color: #713f12;">파트너사</span>
|
|
@endif
|
|
</div>
|
|
<h2 class="text-xl font-bold">{{ $currentTenant->company_name }}</h2>
|
|
</div>
|
|
</div>
|
|
@if($barobillMember)
|
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 text-sm">
|
|
<div class="rounded-lg p-2" style="background: rgba(255,255,255,0.1);">
|
|
<p class="text-xs" style="color: rgba(255,255,255,0.6);">사업자번호</p>
|
|
<p class="font-medium">{{ $barobillMember->formatted_biz_no }}</p>
|
|
</div>
|
|
<div class="rounded-lg p-2" style="background: rgba(255,255,255,0.1);">
|
|
<p class="text-xs" style="color: rgba(255,255,255,0.6);">대표자</p>
|
|
<p class="font-medium">{{ $barobillMember->ceo_name ?? '-' }}</p>
|
|
</div>
|
|
<div class="rounded-lg p-2" style="background: rgba(255,255,255,0.1);">
|
|
<p class="text-xs" style="color: rgba(255,255,255,0.6);">담당자</p>
|
|
<p class="font-medium">{{ $barobillMember->manager_name ?? '-' }}</p>
|
|
</div>
|
|
<div class="rounded-lg p-2" style="background: rgba(255,255,255,0.1);">
|
|
<p class="text-xs" style="color: rgba(255,255,255,0.6);">바로빌 ID</p>
|
|
<p class="font-medium">{{ $barobillMember->barobill_id }}</p>
|
|
</div>
|
|
</div>
|
|
@else
|
|
<div class="flex items-center gap-2" style="color: #fef08a;">
|
|
<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 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>
|
|
<span class="text-sm">바로빌 회원사 미연동</span>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<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>
|
|
<p class="text-xs text-amber-600 mt-1">* 운영서버를 사용하는 고객만 표시됩니다 (테스트 모드 제외)</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onclick="exportExcel()"
|
|
class="bg-green-600 hover:bg-green-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 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
|
|
엑셀 다운로드
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 통계 카드 -->
|
|
<div id="stats-container"
|
|
hx-get="/api/admin/barobill/usage/stats"
|
|
hx-trigger="load, usageUpdated from:body"
|
|
hx-include="#usageFilterForm"
|
|
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
|
|
class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6 flex-shrink-0">
|
|
@include('barobill.usage.partials.stats-skeleton')
|
|
</div>
|
|
|
|
<!-- 필터 영역 -->
|
|
<div class="flex-shrink-0">
|
|
<x-filter-collapsible id="usageFilterForm">
|
|
<form id="usageFilterForm" class="flex flex-wrap gap-2 sm:gap-4 items-center">
|
|
<!-- 전체 테넌트 보기 토글 -->
|
|
<label class="flex items-center gap-2 px-3 py-2 bg-purple-50 border border-purple-200 rounded-lg cursor-pointer hover:bg-purple-100 transition-colors">
|
|
<input type="checkbox"
|
|
name="all_tenants"
|
|
value="1"
|
|
id="allTenantsToggle"
|
|
class="w-4 h-4 rounded border-purple-300 text-purple-600 focus:ring-purple-500">
|
|
<span class="text-sm font-medium text-purple-700">전체 테넌트</span>
|
|
</label>
|
|
|
|
<!-- 시작일 -->
|
|
<div class="flex items-center gap-2">
|
|
<label for="startDate" class="text-sm font-medium text-gray-700 whitespace-nowrap">시작일</label>
|
|
<input type="date"
|
|
name="start_date"
|
|
id="startDate"
|
|
value="{{ now()->startOfMonth()->format('Y-m-d') }}"
|
|
class="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm">
|
|
</div>
|
|
|
|
<span class="text-gray-400">~</span>
|
|
|
|
<!-- 종료일 -->
|
|
<div class="flex items-center gap-2">
|
|
<label for="endDate" class="text-sm font-medium text-gray-700 whitespace-nowrap">종료일</label>
|
|
<input type="date"
|
|
name="end_date"
|
|
id="endDate"
|
|
value="{{ now()->format('Y-m-d') }}"
|
|
class="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm">
|
|
</div>
|
|
|
|
<!-- 빠른 선택 버튼들 -->
|
|
<div class="flex gap-1">
|
|
<button type="button" onclick="setQuickPeriod('thisMonth')" class="px-3 py-2 text-xs font-medium text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition">
|
|
이번달
|
|
</button>
|
|
<button type="button" onclick="setQuickPeriod('lastMonth')" class="px-3 py-2 text-xs font-medium text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition">
|
|
지난달
|
|
</button>
|
|
<button type="button" onclick="setQuickPeriod('last3Months')" class="px-3 py-2 text-xs font-medium text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition">
|
|
최근 3개월
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 조회 버튼 -->
|
|
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition 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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
</svg>
|
|
조회
|
|
</button>
|
|
</form>
|
|
</x-filter-collapsible>
|
|
</div>
|
|
|
|
<!-- 테이블 영역 (HTMX로 로드) -->
|
|
<div id="usage-table"
|
|
hx-get="/api/admin/barobill/usage"
|
|
hx-trigger="load, usageUpdated from:body"
|
|
hx-include="#usageFilterForm"
|
|
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
|
|
class="bg-white rounded-lg shadow-sm overflow-hidden flex-1 flex flex-col min-h-0">
|
|
<!-- 로딩 스피너 -->
|
|
<div class="flex justify-center items-center p-12">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|
</div>
|
|
</div>
|
|
</div><!-- end of flex container -->
|
|
|
|
<!-- 상세 보기 모달 -->
|
|
<div id="detailModal" class="fixed inset-0 z-50 hidden">
|
|
<div class="fixed inset-0 bg-black/50" onclick="UsageDetail.close()"></div>
|
|
<div class="fixed inset-0 flex items-center justify-center p-4">
|
|
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-y-auto" onclick="event.stopPropagation()">
|
|
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between sticky top-0 bg-white">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-gray-800">사용량 상세</h3>
|
|
<p class="text-sm text-gray-500" id="detailModalMemberName"></p>
|
|
</div>
|
|
<button type="button" onclick="UsageDetail.close()" class="text-gray-400 hover:text-gray-600">
|
|
<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="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div id="detailModalContent" class="p-6">
|
|
<!-- 동적으로 채워짐 -->
|
|
</div>
|
|
<div class="px-6 py-4 border-t border-gray-100">
|
|
<button
|
|
type="button"
|
|
onclick="UsageDetail.close()"
|
|
class="w-full px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition"
|
|
>
|
|
닫기
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script>
|
|
// 폼 제출 시 HTMX 이벤트 트리거
|
|
document.getElementById('usageFilterForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
htmx.trigger(document.body, 'usageUpdated');
|
|
});
|
|
|
|
// 빠른 기간 선택
|
|
function setQuickPeriod(period) {
|
|
const today = new Date();
|
|
let startDate, endDate;
|
|
|
|
switch (period) {
|
|
case 'thisMonth':
|
|
startDate = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
endDate = today;
|
|
break;
|
|
case 'lastMonth':
|
|
startDate = new Date(today.getFullYear(), today.getMonth() - 1, 1);
|
|
endDate = new Date(today.getFullYear(), today.getMonth(), 0);
|
|
break;
|
|
case 'last3Months':
|
|
startDate = new Date(today.getFullYear(), today.getMonth() - 2, 1);
|
|
endDate = today;
|
|
break;
|
|
}
|
|
|
|
document.getElementById('startDate').value = formatDate(startDate);
|
|
document.getElementById('endDate').value = formatDate(endDate);
|
|
}
|
|
|
|
function formatDate(date) {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
// 엑셀 다운로드
|
|
function exportExcel() {
|
|
const form = document.getElementById('usageFilterForm');
|
|
const formData = new FormData(form);
|
|
const params = new URLSearchParams(formData);
|
|
|
|
window.location.href = `/api/admin/barobill/usage/export?${params.toString()}`;
|
|
}
|
|
|
|
// 상세 모달 관리
|
|
const UsageDetail = {
|
|
async show(memberId, memberName) {
|
|
document.getElementById('detailModalMemberName').textContent = memberName;
|
|
document.getElementById('detailModalContent').innerHTML = `
|
|
<div class="flex justify-center py-8">
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
</div>
|
|
`;
|
|
document.getElementById('detailModal').classList.remove('hidden');
|
|
|
|
try {
|
|
const form = document.getElementById('usageFilterForm');
|
|
const formData = new FormData(form);
|
|
const params = new URLSearchParams(formData);
|
|
|
|
const res = await fetch(`/api/admin/barobill/usage/${memberId}?${params.toString()}`, {
|
|
headers: {
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'text/html',
|
|
'HX-Request': 'true'
|
|
}
|
|
});
|
|
|
|
if (res.ok) {
|
|
const html = await res.text();
|
|
document.getElementById('detailModalContent').innerHTML = html;
|
|
} else {
|
|
throw new Error('조회 실패');
|
|
}
|
|
} catch (error) {
|
|
document.getElementById('detailModalContent').innerHTML = `
|
|
<div class="text-center text-red-500 py-8">
|
|
<p>상세 정보를 불러오는데 실패했습니다.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
},
|
|
|
|
close() {
|
|
document.getElementById('detailModal').classList.add('hidden');
|
|
}
|
|
};
|
|
|
|
// ESC 키로 모달 닫기
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
UsageDetail.close();
|
|
}
|
|
});
|
|
</script>
|
|
@endpush
|