- processes 테이블: p.name → p.process_name 컬럼명 수정 - 근태: users.department_id → tenant_user_profiles 경유 조인으로 변경 - 직급: users.position → tup.position_key 컬럼 수정 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
741 lines
27 KiB
PHP
741 lines
27 KiB
PHP
<?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',
|
|
};
|
|
}
|
|
}
|