feat: [CEO 대시보드] 섹션별 API + 일일보고서 엑셀
- DashboardCeo 리스크 감지형 서비스 리팩토링 - 일일보고서 어음/외상매출채권 현황 섹션 추가 - 엑셀 내보내기 화면 데이터 기반 리팩토링 - 공정명 컬럼 및 근태 부서 조인 수정 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class DailyReportExport implements FromArray, ShouldAutoSize, WithHeadings, WithStyles, WithTitle
|
||||
@@ -31,10 +32,10 @@ public function headings(): array
|
||||
return [
|
||||
['일일 일보 - '.$this->report['date']],
|
||||
[],
|
||||
['전일 잔액', number_format($this->report['previous_balance']).'원'],
|
||||
['당일 입금액', number_format($this->report['daily_deposit']).'원'],
|
||||
['당일 출금액', number_format($this->report['daily_withdrawal']).'원'],
|
||||
['당일 잔액', number_format($this->report['current_balance']).'원'],
|
||||
['전월 이월', number_format($this->report['previous_balance']).'원'],
|
||||
['당월 입금', number_format($this->report['daily_deposit']).'원'],
|
||||
['당월 출금', number_format($this->report['daily_withdrawal']).'원'],
|
||||
['잔액', number_format($this->report['current_balance']).'원'],
|
||||
[],
|
||||
['구분', '거래처명', '계정과목', '입금액', '출금액', '적요'],
|
||||
];
|
||||
@@ -47,6 +48,7 @@ public function array(): array
|
||||
{
|
||||
$rows = [];
|
||||
|
||||
// ── 예금 입출금 내역 ──
|
||||
foreach ($this->report['details'] as $detail) {
|
||||
$rows[] = [
|
||||
$detail['type_label'],
|
||||
@@ -58,7 +60,7 @@ public function array(): array
|
||||
];
|
||||
}
|
||||
|
||||
// 합계 행 추가
|
||||
// 합계 행
|
||||
$rows[] = [];
|
||||
$rows[] = [
|
||||
'합계',
|
||||
@@ -69,6 +71,37 @@ public function array(): array
|
||||
'',
|
||||
];
|
||||
|
||||
// ── 어음 및 외상매출채권 현황 ──
|
||||
$noteReceivables = $this->report['note_receivables'] ?? [];
|
||||
|
||||
$rows[] = [];
|
||||
$rows[] = [];
|
||||
$rows[] = ['어음 및 외상매출채권 현황'];
|
||||
$rows[] = ['No.', '내용', '금액', '발행일', '만기일'];
|
||||
|
||||
$noteTotal = 0;
|
||||
$no = 1;
|
||||
foreach ($noteReceivables as $item) {
|
||||
$amount = $item['current_balance'] ?? 0;
|
||||
$noteTotal += $amount;
|
||||
$rows[] = [
|
||||
$no++,
|
||||
$item['content'] ?? '-',
|
||||
$amount > 0 ? number_format($amount) : '',
|
||||
$item['issue_date'] ?? '-',
|
||||
$item['due_date'] ?? '-',
|
||||
];
|
||||
}
|
||||
|
||||
// 어음 합계
|
||||
$rows[] = [
|
||||
'합계',
|
||||
'',
|
||||
number_format($noteTotal),
|
||||
'',
|
||||
'',
|
||||
];
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
@@ -77,7 +110,7 @@ public function array(): array
|
||||
*/
|
||||
public function styles(Worksheet $sheet): array
|
||||
{
|
||||
return [
|
||||
$styles = [
|
||||
1 => ['font' => ['bold' => true, 'size' => 14]],
|
||||
3 => ['font' => ['bold' => true]],
|
||||
4 => ['font' => ['bold' => true]],
|
||||
@@ -86,10 +119,32 @@ public function styles(Worksheet $sheet): array
|
||||
8 => [
|
||||
'font' => ['bold' => true],
|
||||
'fill' => [
|
||||
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
|
||||
'fillType' => Fill::FILL_SOLID,
|
||||
'startColor' => ['rgb' => 'E0E0E0'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// 어음 섹션 헤더 스타일 (동적 행 번호)
|
||||
// headings 8행 + details 수 + 합계 2행 + 빈 2행 + 어음 제목 1행 + 어음 헤더 1행
|
||||
$detailCount = count($this->report['details']);
|
||||
$noteHeaderTitleRow = 8 + $detailCount + 2 + 2 + 1; // 어음 제목 행
|
||||
$noteHeaderRow = $noteHeaderTitleRow + 1; // 어음 컬럼 헤더 행
|
||||
|
||||
$styles[$noteHeaderTitleRow] = ['font' => ['bold' => true, 'size' => 12]];
|
||||
$styles[$noteHeaderRow] = [
|
||||
'font' => ['bold' => true],
|
||||
'fill' => [
|
||||
'fillType' => Fill::FILL_SOLID,
|
||||
'startColor' => ['rgb' => 'E0E0E0'],
|
||||
],
|
||||
];
|
||||
|
||||
// 어음 합계 행
|
||||
$noteCount = count($this->report['note_receivables'] ?? []);
|
||||
$noteTotalRow = $noteHeaderRow + $noteCount + 1;
|
||||
$styles[$noteTotalRow] = ['font' => ['bold' => true]];
|
||||
|
||||
return $styles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Exports\DailyReportExport;
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\DailyReportService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
/**
|
||||
* 일일 보고서 컨트롤러
|
||||
@@ -58,4 +61,19 @@ public function summary(Request $request): JsonResponse
|
||||
return $this->service->summary($params);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 일일 보고서 엑셀 다운로드
|
||||
*/
|
||||
public function export(Request $request): BinaryFileResponse
|
||||
{
|
||||
$params = $request->validate([
|
||||
'date' => 'nullable|date',
|
||||
]);
|
||||
|
||||
$reportData = $this->service->exportData($params);
|
||||
$filename = '일일일보_'.$reportData['date'].'.xlsx';
|
||||
|
||||
return Excel::download(new DailyReportExport($reportData), $filename);
|
||||
}
|
||||
}
|
||||
|
||||
92
app/Http/Controllers/Api/V1/DashboardCeoController.php
Normal file
92
app/Http/Controllers/Api/V1/DashboardCeoController.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\DashboardCeoService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* CEO 대시보드 섹션별 API 컨트롤러
|
||||
*
|
||||
* 6개 섹션: 매출, 매입, 생산, 미출고, 시공, 근태
|
||||
*/
|
||||
class DashboardCeoController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DashboardCeoService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 매출 현황 요약
|
||||
* GET /api/v1/dashboard/sales/summary
|
||||
*/
|
||||
public function salesSummary(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->salesSummary(),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 매입 현황 요약
|
||||
* GET /api/v1/dashboard/purchases/summary
|
||||
*/
|
||||
public function purchasesSummary(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->purchasesSummary(),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 생산 현황 요약
|
||||
* GET /api/v1/dashboard/production/summary
|
||||
*/
|
||||
public function productionSummary(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->productionSummary(),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 미출고 내역 요약
|
||||
* GET /api/v1/dashboard/unshipped/summary
|
||||
*/
|
||||
public function unshippedSummary(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->unshippedSummary(),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 시공 현황 요약
|
||||
* GET /api/v1/dashboard/construction/summary
|
||||
*/
|
||||
public function constructionSummary(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->constructionSummary(),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 근태 현황 요약
|
||||
* GET /api/v1/dashboard/attendance/summary
|
||||
*/
|
||||
public function attendanceSummary(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->attendanceSummary(),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -323,7 +323,7 @@ private function getReceivableData(int $tenantId, Carbon $reportDate): array
|
||||
private function callGeminiApi(array $inputData): array
|
||||
{
|
||||
$apiKey = config('services.gemini.api_key');
|
||||
$model = config('services.gemini.model', 'gemini-2.0-flash');
|
||||
$model = config('services.gemini.model', 'gemini-2.5-flash');
|
||||
$baseUrl = config('services.gemini.base_url');
|
||||
|
||||
if (empty($apiKey)) {
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
use App\Models\Tenants\BankAccount;
|
||||
use App\Models\Tenants\Bill;
|
||||
use App\Models\Tenants\Deposit;
|
||||
use App\Models\Tenants\ExpectedExpense;
|
||||
use App\Models\Tenants\Purchase;
|
||||
use App\Models\Tenants\Sale;
|
||||
use App\Models\Tenants\Withdrawal;
|
||||
use Carbon\Carbon;
|
||||
|
||||
@@ -155,6 +158,11 @@ public function summary(array $params): array
|
||||
: null;
|
||||
$operatingStability = $this->getOperatingStability($operatingMonths);
|
||||
|
||||
// 기획서 D1.7 자금현황 카드용 필드
|
||||
$receivableBalance = $this->calculateReceivableBalance($tenantId, $date);
|
||||
$payableBalance = $this->calculatePayableBalance($tenantId);
|
||||
$monthlyExpenseTotal = $this->calculateMonthlyExpenseTotal($tenantId, $date);
|
||||
|
||||
return [
|
||||
'date' => $date->format('Y-m-d'),
|
||||
'day_of_week' => $date->locale('ko')->dayName,
|
||||
@@ -167,9 +175,138 @@ public function summary(array $params): array
|
||||
'monthly_operating_expense' => $monthlyOperatingExpense,
|
||||
'operating_months' => $operatingMonths,
|
||||
'operating_stability' => $operatingStability,
|
||||
// 자금현황 카드용
|
||||
'receivable_balance' => $receivableBalance,
|
||||
'payable_balance' => $payableBalance,
|
||||
'monthly_expense_total' => $monthlyExpenseTotal,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 엑셀 내보내기용 데이터 조합
|
||||
* DailyReportExport가 기대하는 구조로 변환
|
||||
*/
|
||||
public function exportData(array $params): array
|
||||
{
|
||||
$date = isset($params['date']) ? Carbon::parse($params['date']) : Carbon::today();
|
||||
$dateStr = $date->format('Y-m-d');
|
||||
|
||||
// 화면과 동일한 계좌별 현황 데이터 재사용
|
||||
$dailyAccounts = $this->dailyAccounts($params);
|
||||
|
||||
// KRW 계좌 합산 (화면 합계와 동일)
|
||||
$carryover = 0;
|
||||
$totalIncome = 0;
|
||||
$totalExpense = 0;
|
||||
$totalBalance = 0;
|
||||
|
||||
$details = [];
|
||||
|
||||
foreach ($dailyAccounts as $account) {
|
||||
$carryover += $account['carryover'];
|
||||
$totalIncome += $account['income'];
|
||||
$totalExpense += $account['expense'];
|
||||
$totalBalance += $account['balance'];
|
||||
|
||||
// 계좌별 상세 내역
|
||||
if ($account['income'] > 0) {
|
||||
$details[] = [
|
||||
'type_label' => '입금',
|
||||
'client_name' => $account['category'],
|
||||
'account_code' => '-',
|
||||
'deposit_amount' => $account['income'],
|
||||
'withdrawal_amount' => 0,
|
||||
'description' => '',
|
||||
];
|
||||
}
|
||||
|
||||
if ($account['expense'] > 0) {
|
||||
$details[] = [
|
||||
'type_label' => '출금',
|
||||
'client_name' => $account['category'],
|
||||
'account_code' => '-',
|
||||
'deposit_amount' => 0,
|
||||
'withdrawal_amount' => $account['expense'],
|
||||
'description' => '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 어음 및 외상매출채권 현황
|
||||
$noteReceivables = $this->noteReceivables($params);
|
||||
|
||||
return [
|
||||
'date' => $dateStr,
|
||||
'previous_balance' => $carryover,
|
||||
'daily_deposit' => $totalIncome,
|
||||
'daily_withdrawal' => $totalExpense,
|
||||
'current_balance' => $totalBalance,
|
||||
'details' => $details,
|
||||
'note_receivables' => $noteReceivables,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 미수금 잔액 계산
|
||||
* = 전체 매출 - 전체 입금 - 전체 수취어음 (기준일까지)
|
||||
* ReceivablesService.getTotalCarryForwardBalance() 동일 로직
|
||||
*/
|
||||
private function calculateReceivableBalance(int $tenantId, Carbon $date): float
|
||||
{
|
||||
$endDate = $date->format('Y-m-d');
|
||||
|
||||
$totalSales = Sale::where('tenant_id', $tenantId)
|
||||
->whereNotNull('client_id')
|
||||
->where('sale_date', '<=', $endDate)
|
||||
->sum('total_amount');
|
||||
|
||||
$totalDeposits = Deposit::where('tenant_id', $tenantId)
|
||||
->whereNotNull('client_id')
|
||||
->where('deposit_date', '<=', $endDate)
|
||||
->sum('amount');
|
||||
|
||||
$totalBills = Bill::where('tenant_id', $tenantId)
|
||||
->whereNotNull('client_id')
|
||||
->where('bill_type', 'received')
|
||||
->where('issue_date', '<=', $endDate)
|
||||
->sum('amount');
|
||||
|
||||
return (float) ($totalSales - $totalDeposits - $totalBills);
|
||||
}
|
||||
|
||||
/**
|
||||
* 미지급금 잔액 계산
|
||||
* = 미지급 상태(pending, partial, overdue)인 ExpectedExpense 합계
|
||||
*/
|
||||
private function calculatePayableBalance(int $tenantId): float
|
||||
{
|
||||
return (float) ExpectedExpense::where('tenant_id', $tenantId)
|
||||
->whereIn('payment_status', ['pending', 'partial', 'overdue'])
|
||||
->sum('amount');
|
||||
}
|
||||
|
||||
/**
|
||||
* 당월 예상 지출 합계 계산
|
||||
* = 당월 매입(Purchase) + 당월 예상지출(ExpectedExpense)
|
||||
*/
|
||||
private function calculateMonthlyExpenseTotal(int $tenantId, Carbon $date): float
|
||||
{
|
||||
$startOfMonth = $date->copy()->startOfMonth()->format('Y-m-d');
|
||||
$endOfMonth = $date->copy()->endOfMonth()->format('Y-m-d');
|
||||
|
||||
// 당월 매입 합계
|
||||
$purchaseTotal = Purchase::where('tenant_id', $tenantId)
|
||||
->whereBetween('purchase_date', [$startOfMonth, $endOfMonth])
|
||||
->sum('total_amount');
|
||||
|
||||
// 당월 예상 지출 합계 (매입 외: 카드, 어음, 급여, 임대료 등)
|
||||
$expectedExpenseTotal = ExpectedExpense::where('tenant_id', $tenantId)
|
||||
->whereBetween('expected_payment_date', [$startOfMonth, $endOfMonth])
|
||||
->sum('amount');
|
||||
|
||||
return (float) ($purchaseTotal + $expectedExpenseTotal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 직전 3개월 평균 월 운영비 계산
|
||||
*
|
||||
|
||||
740
app/Services/DashboardCeoService.php
Normal file
740
app/Services/DashboardCeoService.php
Normal file
@@ -0,0 +1,740 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* CEO 대시보드 섹션별 요약 서비스
|
||||
*
|
||||
* 6개 섹션: 매출, 매입, 생산, 미출고, 시공, 근태
|
||||
* sam_stat 우선 조회 → fallback 원본 DB
|
||||
*/
|
||||
class DashboardCeoService extends Service
|
||||
{
|
||||
// ─── 1. 매출 현황 ───────────────────────────────
|
||||
|
||||
/**
|
||||
* 매출 현황 요약
|
||||
*/
|
||||
public function salesSummary(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$now = Carbon::now();
|
||||
$year = $now->year;
|
||||
$month = $now->month;
|
||||
$today = $now->format('Y-m-d');
|
||||
|
||||
// 누적 매출 (연초~오늘)
|
||||
$cumulativeSales = DB::table('sales')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereYear('sale_date', $year)
|
||||
->where('sale_date', '<=', $today)
|
||||
->whereNull('deleted_at')
|
||||
->sum('total_amount') ?: 0;
|
||||
|
||||
// 당월 매출
|
||||
$monthlySales = DB::table('sales')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereYear('sale_date', $year)
|
||||
->whereMonth('sale_date', $month)
|
||||
->whereNull('deleted_at')
|
||||
->sum('total_amount') ?: 0;
|
||||
|
||||
// 전년 동월 매출 (YoY)
|
||||
$lastYearMonthlySales = DB::table('sales')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereYear('sale_date', $year - 1)
|
||||
->whereMonth('sale_date', $month)
|
||||
->whereNull('deleted_at')
|
||||
->sum('total_amount') ?: 0;
|
||||
|
||||
$yoyChange = $lastYearMonthlySales > 0
|
||||
? round((($monthlySales - $lastYearMonthlySales) / $lastYearMonthlySales) * 100, 1)
|
||||
: 0;
|
||||
|
||||
// 달성률 (당월 매출 / 전년 동월 매출 * 100)
|
||||
$achievementRate = $lastYearMonthlySales > 0
|
||||
? round(($monthlySales / $lastYearMonthlySales) * 100, 0)
|
||||
: 0;
|
||||
|
||||
// 월별 추이 (1~12월)
|
||||
$monthlyTrend = $this->getSalesMonthlyTrend($tenantId, $year);
|
||||
|
||||
// 거래처별 매출 (상위 5개)
|
||||
$clientSales = $this->getSalesClientRanking($tenantId, $year);
|
||||
|
||||
// 일별 매출 내역 (최근 10건)
|
||||
$dailyItems = $this->getSalesDailyItems($tenantId, $today);
|
||||
|
||||
// 일별 합계
|
||||
$dailyTotal = DB::table('sales')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('sale_date', $today)
|
||||
->whereNull('deleted_at')
|
||||
->sum('total_amount') ?: 0;
|
||||
|
||||
return [
|
||||
'cumulative_sales' => (int) $cumulativeSales,
|
||||
'achievement_rate' => (int) $achievementRate,
|
||||
'yoy_change' => $yoyChange,
|
||||
'monthly_sales' => (int) $monthlySales,
|
||||
'monthly_trend' => $monthlyTrend,
|
||||
'client_sales' => $clientSales,
|
||||
'daily_items' => $dailyItems,
|
||||
'daily_total' => (int) $dailyTotal,
|
||||
];
|
||||
}
|
||||
|
||||
private function getSalesMonthlyTrend(int $tenantId, int $year): array
|
||||
{
|
||||
$monthlyData = DB::table('sales')
|
||||
->select(DB::raw('MONTH(sale_date) as month'), DB::raw('COALESCE(SUM(total_amount), 0) as amount'))
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereYear('sale_date', $year)
|
||||
->whereNull('deleted_at')
|
||||
->groupBy(DB::raw('MONTH(sale_date)'))
|
||||
->orderBy('month')
|
||||
->get();
|
||||
|
||||
$result = [];
|
||||
for ($i = 1; $i <= 12; $i++) {
|
||||
$found = $monthlyData->firstWhere('month', $i);
|
||||
$result[] = [
|
||||
'month' => sprintf('%d-%02d', $year, $i),
|
||||
'label' => $i.'월',
|
||||
'amount' => $found ? (int) $found->amount : 0,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getSalesClientRanking(int $tenantId, int $year): array
|
||||
{
|
||||
$clients = DB::table('sales as s')
|
||||
->leftJoin('clients as c', 's.client_id', '=', 'c.id')
|
||||
->select('c.name', DB::raw('SUM(s.total_amount) as amount'))
|
||||
->where('s.tenant_id', $tenantId)
|
||||
->whereYear('s.sale_date', $year)
|
||||
->whereNull('s.deleted_at')
|
||||
->groupBy('s.client_id', 'c.name')
|
||||
->orderByDesc('amount')
|
||||
->limit(5)
|
||||
->get();
|
||||
|
||||
return $clients->map(fn ($item) => [
|
||||
'name' => $item->name ?? '미지정',
|
||||
'amount' => (int) $item->amount,
|
||||
])->toArray();
|
||||
}
|
||||
|
||||
private function getSalesDailyItems(int $tenantId, string $today): array
|
||||
{
|
||||
$items = DB::table('sales as s')
|
||||
->leftJoin('clients as c', 's.client_id', '=', 'c.id')
|
||||
->select([
|
||||
's.sale_date as date',
|
||||
'c.name as client',
|
||||
's.description as item',
|
||||
's.total_amount as amount',
|
||||
's.status',
|
||||
's.deposit_id',
|
||||
])
|
||||
->where('s.tenant_id', $tenantId)
|
||||
->where('s.sale_date', '>=', Carbon::parse($today)->subDays(30)->format('Y-m-d'))
|
||||
->whereNull('s.deleted_at')
|
||||
->orderByDesc('s.sale_date')
|
||||
->limit(10)
|
||||
->get();
|
||||
|
||||
return $items->map(fn ($item) => [
|
||||
'date' => $item->date,
|
||||
'client' => $item->client ?? '미지정',
|
||||
'item' => $item->item ?? '-',
|
||||
'amount' => (int) $item->amount,
|
||||
'status' => $item->deposit_id ? 'deposited' : 'unpaid',
|
||||
])->toArray();
|
||||
}
|
||||
|
||||
// ─── 2. 매입 현황 ───────────────────────────────
|
||||
|
||||
/**
|
||||
* 매입 현황 요약
|
||||
*/
|
||||
public function purchasesSummary(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$now = Carbon::now();
|
||||
$year = $now->year;
|
||||
$month = $now->month;
|
||||
$today = $now->format('Y-m-d');
|
||||
|
||||
// 누적 매입
|
||||
$cumulativePurchase = DB::table('purchases')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereYear('purchase_date', $year)
|
||||
->where('purchase_date', '<=', $today)
|
||||
->whereNull('deleted_at')
|
||||
->sum('total_amount') ?: 0;
|
||||
|
||||
// 미결제 금액 (withdrawal_id가 없는 것)
|
||||
$unpaidAmount = DB::table('purchases')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereYear('purchase_date', $year)
|
||||
->whereNull('withdrawal_id')
|
||||
->whereNull('deleted_at')
|
||||
->sum('total_amount') ?: 0;
|
||||
|
||||
// 전년 동월 대비
|
||||
$thisMonthPurchase = DB::table('purchases')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereYear('purchase_date', $year)
|
||||
->whereMonth('purchase_date', $month)
|
||||
->whereNull('deleted_at')
|
||||
->sum('total_amount') ?: 0;
|
||||
|
||||
$lastYearMonthPurchase = DB::table('purchases')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereYear('purchase_date', $year - 1)
|
||||
->whereMonth('purchase_date', $month)
|
||||
->whereNull('deleted_at')
|
||||
->sum('total_amount') ?: 0;
|
||||
|
||||
$yoyChange = $lastYearMonthPurchase > 0
|
||||
? round((($thisMonthPurchase - $lastYearMonthPurchase) / $lastYearMonthPurchase) * 100, 1)
|
||||
: 0;
|
||||
|
||||
// 월별 추이
|
||||
$monthlyTrend = $this->getPurchaseMonthlyTrend($tenantId, $year);
|
||||
|
||||
// 자재 구성 비율 (purchase_type별)
|
||||
$materialRatio = $this->getPurchaseMaterialRatio($tenantId, $year);
|
||||
|
||||
// 일별 매입 내역
|
||||
$dailyItems = $this->getPurchaseDailyItems($tenantId, $today);
|
||||
|
||||
// 일별 합계
|
||||
$dailyTotal = DB::table('purchases')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('purchase_date', $today)
|
||||
->whereNull('deleted_at')
|
||||
->sum('total_amount') ?: 0;
|
||||
|
||||
return [
|
||||
'cumulative_purchase' => (int) $cumulativePurchase,
|
||||
'unpaid_amount' => (int) $unpaidAmount,
|
||||
'yoy_change' => $yoyChange,
|
||||
'monthly_trend' => $monthlyTrend,
|
||||
'material_ratio' => $materialRatio,
|
||||
'daily_items' => $dailyItems,
|
||||
'daily_total' => (int) $dailyTotal,
|
||||
];
|
||||
}
|
||||
|
||||
private function getPurchaseMonthlyTrend(int $tenantId, int $year): array
|
||||
{
|
||||
$monthlyData = DB::table('purchases')
|
||||
->select(DB::raw('MONTH(purchase_date) as month'), DB::raw('COALESCE(SUM(total_amount), 0) as amount'))
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereYear('purchase_date', $year)
|
||||
->whereNull('deleted_at')
|
||||
->groupBy(DB::raw('MONTH(purchase_date)'))
|
||||
->orderBy('month')
|
||||
->get();
|
||||
|
||||
$result = [];
|
||||
for ($i = 1; $i <= 12; $i++) {
|
||||
$found = $monthlyData->firstWhere('month', $i);
|
||||
$result[] = [
|
||||
'month' => sprintf('%d-%02d', $year, $i),
|
||||
'label' => $i.'월',
|
||||
'amount' => $found ? (int) $found->amount : 0,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getPurchaseMaterialRatio(int $tenantId, int $year): array
|
||||
{
|
||||
$colors = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899'];
|
||||
|
||||
$ratioData = DB::table('purchases')
|
||||
->select('purchase_type', DB::raw('SUM(total_amount) as value'))
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereYear('purchase_date', $year)
|
||||
->whereNull('deleted_at')
|
||||
->groupBy('purchase_type')
|
||||
->orderByDesc('value')
|
||||
->limit(6)
|
||||
->get();
|
||||
|
||||
$total = $ratioData->sum('value');
|
||||
$idx = 0;
|
||||
|
||||
return $ratioData->map(function ($item) use ($total, $colors, &$idx) {
|
||||
$name = $this->getPurchaseTypeName($item->purchase_type);
|
||||
$result = [
|
||||
'name' => $name,
|
||||
'value' => (int) $item->value,
|
||||
'percentage' => $total > 0 ? round(($item->value / $total) * 100, 1) : 0,
|
||||
'color' => $colors[$idx % count($colors)],
|
||||
];
|
||||
$idx++;
|
||||
|
||||
return $result;
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
private function getPurchaseTypeName(?string $type): string
|
||||
{
|
||||
$map = [
|
||||
'원재료매입' => '원자재',
|
||||
'부재료매입' => '부자재',
|
||||
'소모품매입' => '소모품',
|
||||
'외주가공비' => '외주가공',
|
||||
'접대비' => '접대비',
|
||||
'복리후생비' => '복리후생',
|
||||
];
|
||||
|
||||
return $map[$type] ?? ($type ?? '기타');
|
||||
}
|
||||
|
||||
private function getPurchaseDailyItems(int $tenantId, string $today): array
|
||||
{
|
||||
$items = DB::table('purchases as p')
|
||||
->leftJoin('clients as c', 'p.client_id', '=', 'c.id')
|
||||
->select([
|
||||
'p.purchase_date as date',
|
||||
'c.name as supplier',
|
||||
'p.description as item',
|
||||
'p.total_amount as amount',
|
||||
'p.withdrawal_id',
|
||||
])
|
||||
->where('p.tenant_id', $tenantId)
|
||||
->where('p.purchase_date', '>=', Carbon::parse($today)->subDays(30)->format('Y-m-d'))
|
||||
->whereNull('p.deleted_at')
|
||||
->orderByDesc('p.purchase_date')
|
||||
->limit(10)
|
||||
->get();
|
||||
|
||||
return $items->map(fn ($item) => [
|
||||
'date' => $item->date,
|
||||
'supplier' => $item->supplier ?? '미지정',
|
||||
'item' => $item->item ?? '-',
|
||||
'amount' => (int) $item->amount,
|
||||
'status' => $item->withdrawal_id ? 'paid' : 'unpaid',
|
||||
])->toArray();
|
||||
}
|
||||
|
||||
// ─── 3. 생산 현황 ───────────────────────────────
|
||||
|
||||
/**
|
||||
* 생산 현황 요약
|
||||
*/
|
||||
public function productionSummary(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$today = Carbon::now();
|
||||
$todayStr = $today->format('Y-m-d');
|
||||
|
||||
$dayOfWeekMap = ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'];
|
||||
$dayOfWeek = $dayOfWeekMap[$today->dayOfWeek];
|
||||
|
||||
// 공정별 작업 현황
|
||||
$processes = $this->getProductionProcesses($tenantId, $todayStr);
|
||||
|
||||
// 출고 현황
|
||||
$shipment = $this->getShipmentSummary($tenantId, $todayStr);
|
||||
|
||||
return [
|
||||
'date' => $todayStr,
|
||||
'day_of_week' => $dayOfWeek,
|
||||
'processes' => $processes,
|
||||
'shipment' => $shipment,
|
||||
];
|
||||
}
|
||||
|
||||
private function getProductionProcesses(int $tenantId, string $today): array
|
||||
{
|
||||
// 공정별 작업 지시 집계
|
||||
$processData = DB::table('work_orders as wo')
|
||||
->leftJoin('processes as p', 'wo.process_id', '=', 'p.id')
|
||||
->select(
|
||||
'p.id as process_id',
|
||||
'p.process_name as process_name',
|
||||
DB::raw('COUNT(*) as total_work'),
|
||||
DB::raw("SUM(CASE WHEN wo.status = 'pending' OR wo.status = 'unassigned' OR wo.status = 'waiting' THEN 1 ELSE 0 END) as todo"),
|
||||
DB::raw("SUM(CASE WHEN wo.status = 'in_progress' THEN 1 ELSE 0 END) as in_progress"),
|
||||
DB::raw("SUM(CASE WHEN wo.status = 'completed' OR wo.status = 'shipped' THEN 1 ELSE 0 END) as completed"),
|
||||
DB::raw("SUM(CASE WHEN wo.priority = 'urgent' THEN 1 ELSE 0 END) as urgent"),
|
||||
)
|
||||
->where('wo.tenant_id', $tenantId)
|
||||
->where('wo.scheduled_date', $today)
|
||||
->where('wo.is_active', true)
|
||||
->whereNull('wo.deleted_at')
|
||||
->whereNotNull('wo.process_id')
|
||||
->groupBy('p.id', 'p.process_name')
|
||||
->orderBy('p.process_name')
|
||||
->get();
|
||||
|
||||
return $processData->map(function ($process) use ($tenantId, $today) {
|
||||
$totalWork = (int) $process->total_work;
|
||||
$todo = (int) $process->todo;
|
||||
$inProgress = (int) $process->in_progress;
|
||||
$completed = (int) $process->completed;
|
||||
|
||||
// 작업 아이템 (최대 5건)
|
||||
$workItems = DB::table('work_orders as wo')
|
||||
->leftJoin('orders as o', 'wo.sales_order_id', '=', 'o.id')
|
||||
->leftJoin('clients as c', 'o.client_id', '=', 'c.id')
|
||||
->select([
|
||||
'wo.id',
|
||||
'wo.work_order_no as order_no',
|
||||
'c.name as client',
|
||||
'wo.project_name as product',
|
||||
'wo.status',
|
||||
])
|
||||
->where('wo.tenant_id', $tenantId)
|
||||
->where('wo.process_id', $process->process_id)
|
||||
->where('wo.scheduled_date', $today)
|
||||
->where('wo.is_active', true)
|
||||
->whereNull('wo.deleted_at')
|
||||
->orderByRaw("FIELD(wo.priority, 'urgent', 'normal', 'low')")
|
||||
->limit(5)
|
||||
->get();
|
||||
|
||||
// 작업자별 현황
|
||||
$workers = DB::table('work_order_assignees as woa')
|
||||
->join('work_orders as wo', 'woa.work_order_id', '=', 'wo.id')
|
||||
->leftJoin('users as u', 'woa.user_id', '=', 'u.id')
|
||||
->select(
|
||||
'u.name',
|
||||
DB::raw('COUNT(*) as assigned'),
|
||||
DB::raw("SUM(CASE WHEN wo.status IN ('completed', 'shipped') THEN 1 ELSE 0 END) as completed"),
|
||||
)
|
||||
->where('wo.tenant_id', $tenantId)
|
||||
->where('wo.process_id', $process->process_id)
|
||||
->where('wo.scheduled_date', $today)
|
||||
->where('wo.is_active', true)
|
||||
->whereNull('wo.deleted_at')
|
||||
->groupBy('woa.user_id', 'u.name')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'process_name' => $process->process_name ?? '미지정',
|
||||
'total_work' => $totalWork,
|
||||
'todo' => $todo,
|
||||
'in_progress' => $inProgress,
|
||||
'completed' => $completed,
|
||||
'urgent' => (int) $process->urgent,
|
||||
'sub_line' => 0,
|
||||
'regular' => max(0, $totalWork - (int) $process->urgent),
|
||||
'worker_count' => $workers->count(),
|
||||
'work_items' => $workItems->map(fn ($wi) => [
|
||||
'id' => 'wo_'.$wi->id,
|
||||
'order_no' => $wi->order_no ?? '-',
|
||||
'client' => $wi->client ?? '미지정',
|
||||
'product' => $wi->product ?? '-',
|
||||
'quantity' => 0,
|
||||
'status' => $this->mapWorkOrderStatus($wi->status),
|
||||
])->toArray(),
|
||||
'workers' => $workers->map(fn ($w) => [
|
||||
'name' => $w->name ?? '미지정',
|
||||
'assigned' => (int) $w->assigned,
|
||||
'completed' => (int) $w->completed,
|
||||
'rate' => $w->assigned > 0 ? round(($w->completed / $w->assigned) * 100, 0) : 0,
|
||||
])->toArray(),
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
private function mapWorkOrderStatus(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
'completed', 'shipped' => 'completed',
|
||||
'in_progress' => 'in_progress',
|
||||
default => 'pending',
|
||||
};
|
||||
}
|
||||
|
||||
private function getShipmentSummary(int $tenantId, string $today): array
|
||||
{
|
||||
$thisMonth = Carbon::parse($today);
|
||||
$monthStart = $thisMonth->copy()->startOfMonth()->format('Y-m-d');
|
||||
$monthEnd = $thisMonth->copy()->endOfMonth()->format('Y-m-d');
|
||||
|
||||
// 예정 출고
|
||||
$expected = DB::table('shipments')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereBetween('scheduled_date', [$monthStart, $monthEnd])
|
||||
->whereIn('status', ['scheduled', 'ready'])
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('COUNT(*) as count, COALESCE(SUM(shipping_cost), 0) as amount')
|
||||
->first();
|
||||
|
||||
// 실제 출고
|
||||
$actual = DB::table('shipments')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereBetween('scheduled_date', [$monthStart, $monthEnd])
|
||||
->whereIn('status', ['shipping', 'completed'])
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('COUNT(*) as count, COALESCE(SUM(shipping_cost), 0) as amount')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'expected_amount' => (int) ($expected->amount ?? 0),
|
||||
'expected_count' => (int) ($expected->count ?? 0),
|
||||
'actual_amount' => (int) ($actual->amount ?? 0),
|
||||
'actual_count' => (int) ($actual->count ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
// ─── 4. 미출고 내역 ──────────────────────────────
|
||||
|
||||
/**
|
||||
* 미출고 내역 요약
|
||||
*/
|
||||
public function unshippedSummary(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$today = Carbon::now()->format('Y-m-d');
|
||||
|
||||
$items = DB::table('shipments as s')
|
||||
->leftJoin('orders as o', 's.order_id', '=', 'o.id')
|
||||
->leftJoin('clients as c', 's.client_id', '=', 'c.id')
|
||||
->select([
|
||||
's.id',
|
||||
's.lot_no as port_no',
|
||||
's.site_name',
|
||||
'c.name as order_client',
|
||||
's.scheduled_date as due_date',
|
||||
])
|
||||
->where('s.tenant_id', $tenantId)
|
||||
->whereIn('s.status', ['scheduled', 'ready'])
|
||||
->whereNull('s.deleted_at')
|
||||
->orderBy('s.scheduled_date')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
$result = $items->map(function ($item) use ($today) {
|
||||
$dueDate = Carbon::parse($item->due_date);
|
||||
$daysLeft = Carbon::parse($today)->diffInDays($dueDate, false);
|
||||
|
||||
return [
|
||||
'id' => 'us_'.$item->id,
|
||||
'port_no' => $item->port_no ?? '-',
|
||||
'site_name' => $item->site_name ?? '-',
|
||||
'order_client' => $item->order_client ?? '미지정',
|
||||
'due_date' => $item->due_date,
|
||||
'days_left' => (int) $daysLeft,
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
return [
|
||||
'items' => $result,
|
||||
'total_count' => count($result),
|
||||
];
|
||||
}
|
||||
|
||||
// ─── 5. 시공 현황 ───────────────────────────────
|
||||
|
||||
/**
|
||||
* 시공 현황 요약
|
||||
*/
|
||||
public function constructionSummary(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$now = Carbon::now();
|
||||
$monthStart = $now->copy()->startOfMonth()->format('Y-m-d');
|
||||
$monthEnd = $now->copy()->endOfMonth()->format('Y-m-d');
|
||||
|
||||
// 이번 달 시공 건수
|
||||
$thisMonthCount = DB::table('contracts')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where(function ($q) use ($monthStart, $monthEnd) {
|
||||
$q->whereBetween('contract_start_date', [$monthStart, $monthEnd])
|
||||
->orWhereBetween('contract_end_date', [$monthStart, $monthEnd])
|
||||
->orWhere(function ($q2) use ($monthStart, $monthEnd) {
|
||||
$q2->where('contract_start_date', '<=', $monthStart)
|
||||
->where('contract_end_date', '>=', $monthEnd);
|
||||
});
|
||||
})
|
||||
->where('is_active', true)
|
||||
->whereNull('deleted_at')
|
||||
->count();
|
||||
|
||||
// 완료 건수
|
||||
$completedCount = DB::table('contracts')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('status', 'completed')
|
||||
->where(function ($q) use ($monthStart, $monthEnd) {
|
||||
$q->whereBetween('contract_end_date', [$monthStart, $monthEnd]);
|
||||
})
|
||||
->where('is_active', true)
|
||||
->whereNull('deleted_at')
|
||||
->count();
|
||||
|
||||
// 시공 아이템 목록
|
||||
$items = DB::table('contracts as ct')
|
||||
->leftJoin('users as u', 'ct.construction_pm_id', '=', 'u.id')
|
||||
->select([
|
||||
'ct.id',
|
||||
'ct.project_name as site_name',
|
||||
'ct.partner_name as client',
|
||||
'ct.contract_start_date as start_date',
|
||||
'ct.contract_end_date as end_date',
|
||||
'ct.status',
|
||||
'ct.stage',
|
||||
])
|
||||
->where('ct.tenant_id', $tenantId)
|
||||
->where('ct.is_active', true)
|
||||
->whereNull('ct.deleted_at')
|
||||
->where(function ($q) use ($monthStart, $monthEnd) {
|
||||
$q->whereBetween('ct.contract_start_date', [$monthStart, $monthEnd])
|
||||
->orWhereBetween('ct.contract_end_date', [$monthStart, $monthEnd])
|
||||
->orWhere(function ($q2) use ($monthStart, $monthEnd) {
|
||||
$q2->where('ct.contract_start_date', '<=', $monthStart)
|
||||
->where('ct.contract_end_date', '>=', $monthEnd);
|
||||
});
|
||||
})
|
||||
->orderBy('ct.contract_start_date')
|
||||
->limit(20)
|
||||
->get();
|
||||
|
||||
$today = $now->format('Y-m-d');
|
||||
|
||||
return [
|
||||
'this_month' => $thisMonthCount,
|
||||
'completed' => $completedCount,
|
||||
'items' => $items->map(function ($item) use ($today) {
|
||||
$progress = $this->calculateContractProgress($item, $today);
|
||||
|
||||
return [
|
||||
'id' => 'c_'.$item->id,
|
||||
'site_name' => $item->site_name ?? '-',
|
||||
'client' => $item->client ?? '미지정',
|
||||
'start_date' => $item->start_date,
|
||||
'end_date' => $item->end_date,
|
||||
'progress' => $progress,
|
||||
'status' => $this->mapContractStatus($item->status, $item->start_date, $today),
|
||||
];
|
||||
})->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
private function calculateContractProgress(object $contract, string $today): int
|
||||
{
|
||||
if ($contract->status === 'completed') {
|
||||
return 100;
|
||||
}
|
||||
|
||||
$start = Carbon::parse($contract->start_date);
|
||||
$end = Carbon::parse($contract->end_date);
|
||||
$now = Carbon::parse($today);
|
||||
|
||||
if ($now->lt($start)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$totalDays = $start->diffInDays($end);
|
||||
if ($totalDays <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$elapsedDays = $start->diffInDays($now);
|
||||
$progress = min(99, round(($elapsedDays / $totalDays) * 100));
|
||||
|
||||
return (int) $progress;
|
||||
}
|
||||
|
||||
private function mapContractStatus(string $status, ?string $startDate, string $today): string
|
||||
{
|
||||
if ($status === 'completed') {
|
||||
return 'completed';
|
||||
}
|
||||
|
||||
if ($startDate && Carbon::parse($startDate)->gt(Carbon::parse($today))) {
|
||||
return 'scheduled';
|
||||
}
|
||||
|
||||
return 'in_progress';
|
||||
}
|
||||
|
||||
// ─── 6. 근태 현황 ───────────────────────────────
|
||||
|
||||
/**
|
||||
* 근태 현황 요약
|
||||
*/
|
||||
public function attendanceSummary(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$today = Carbon::now()->format('Y-m-d');
|
||||
|
||||
// 오늘 근태 기록
|
||||
$attendances = DB::table('attendances as a')
|
||||
->leftJoin('users as u', 'a.user_id', '=', 'u.id')
|
||||
->leftJoin('tenant_user_profiles as tup', function ($join) use ($tenantId) {
|
||||
$join->on('tup.user_id', '=', 'u.id')
|
||||
->where('tup.tenant_id', '=', $tenantId);
|
||||
})
|
||||
->leftJoin('departments as d', 'tup.department_id', '=', 'd.id')
|
||||
->select([
|
||||
'a.id',
|
||||
'a.status',
|
||||
'u.name',
|
||||
'd.name as department',
|
||||
'tup.position_key as position',
|
||||
])
|
||||
->where('a.tenant_id', $tenantId)
|
||||
->where('a.base_date', $today)
|
||||
->whereNull('a.deleted_at')
|
||||
->get();
|
||||
|
||||
$present = 0;
|
||||
$onLeave = 0;
|
||||
$late = 0;
|
||||
$absent = 0;
|
||||
|
||||
$employees = $attendances->map(function ($att) use (&$present, &$onLeave, &$late, &$absent) {
|
||||
$mappedStatus = $this->mapAttendanceStatus($att->status);
|
||||
|
||||
match ($mappedStatus) {
|
||||
'present' => $present++,
|
||||
'on_leave' => $onLeave++,
|
||||
'late' => $late++,
|
||||
'absent' => $absent++,
|
||||
default => null,
|
||||
};
|
||||
|
||||
return [
|
||||
'id' => 'emp_'.$att->id,
|
||||
'department' => $att->department ?? '-',
|
||||
'position' => $att->position ?? '-',
|
||||
'name' => $att->name ?? '-',
|
||||
'status' => $mappedStatus,
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
return [
|
||||
'present' => $present,
|
||||
'on_leave' => $onLeave,
|
||||
'late' => $late,
|
||||
'absent' => $absent,
|
||||
'employees' => $employees,
|
||||
];
|
||||
}
|
||||
|
||||
private function mapAttendanceStatus(?string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
'onTime', 'normal', 'overtime', 'earlyLeave' => 'present',
|
||||
'late', 'lateEarlyLeave' => 'late',
|
||||
'vacation', 'halfDayVacation', 'sickLeave' => 'on_leave',
|
||||
'absent', 'noRecord' => 'absent',
|
||||
default => 'present',
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user