selectRaw('e.id as expense_id, e.amount as expense_amount, e.description, e.receipt_path as expense_receipt, e.retailor, e.purchased_by, u.firstname as purchaser_firstname, u.lastname as purchaser_lastname') ->leftJoin('users as u', 'u.id', '=', 'e.purchased_by') ->leftJoin('reimbursement_batch_items as bi', function ($join) { $join->on('bi.expense_id', '=', 'e.id'); $join->whereNull('bi.unassigned_at'); }) ->whereNull('e.reimbursement_id') ->where('e.category', '!=', 'Donation') ->where(function ($q) { $q->where('e.status', 'approved') ->orWhere('e.status', 'Approved') ->orWhere('e.status', 'APPROVED'); }) ->whereNull('bi.id') ->orderByDesc('e.created_at'); $pendingRows = $pendingQuery->get(); $pendingItems = []; $itemsMap = []; foreach ($pendingRows as $row) { $expenseId = (int) ($row->expense_id ?? 0); if ($expenseId <= 0) { continue; } $receiptUrl = $this->files->expenseReceiptUrl($row->expense_receipt ?? null); $fullName = trim(($row->purchaser_firstname ?? '') . ' ' . ($row->purchaser_lastname ?? '')) ?: 'Unknown'; $item = [ 'id' => $expenseId, 'expense_id' => $expenseId, 'reimbursement_id' => null, 'expense_amount' => (float) ($row->expense_amount ?? 0), 'vendor' => (string) ($row->retailor ?? ''), 'description' => trim((string) ($row->description ?? '')), 'purchased_by' => $fullName, 'receipt_url' => $receiptUrl, 'batch_id' => null, 'batch_label' => null, 'admin_id' => null, 'admin_name' => null, 'batch_number' => 0, ]; $pendingItems[] = $item; $itemsMap[$expenseId] = $item; } $assignedQuery = DB::table('reimbursement_batch_items as bi') ->selectRaw('bi.id as batch_item_id, bi.batch_id, bi.admin_id, bi.reimbursement_id, bi.expense_id, b.title as batch_title, b.status as batch_status, b.yearly_batch_number as batch_year_number, e.amount as expense_amount, e.description, e.receipt_path as expense_receipt, e.retailor, purchaser.firstname as purchaser_firstname, purchaser.lastname as purchaser_lastname, admin.firstname as admin_firstname, admin.lastname as admin_lastname') ->join('reimbursement_batches as b', 'b.id', '=', 'bi.batch_id') ->join('expenses as e', 'e.id', '=', 'bi.expense_id') ->leftJoin('users as purchaser', 'purchaser.id', '=', 'e.purchased_by') ->leftJoin('users as admin', 'admin.id', '=', 'bi.admin_id') ->where('b.status', 'open') ->whereNull('bi.unassigned_at') ->whereNull('e.reimbursement_id') ->where('e.category', '!=', 'Donation') ->where(function ($q) { $q->where('e.status', 'approved') ->orWhere('e.status', 'Approved') ->orWhere('e.status', 'APPROVED'); }) ->orderBy('b.opened_at', 'ASC') ->orderBy('bi.assigned_at', 'ASC'); $assignedRows = $assignedQuery->get(); $batchStructures = []; foreach ($assignedRows as $row) { $expenseId = (int) ($row->expense_id ?? 0); $batchId = (int) ($row->batch_id ?? 0); if ($expenseId <= 0 || $batchId <= 0) { continue; } $receiptUrl = $this->files->expenseReceiptUrl($row->expense_receipt ?? null); $purchaser = trim(($row->purchaser_firstname ?? '') . ' ' . ($row->purchaser_lastname ?? '')) ?: 'Unknown'; $yearlyNumber = (int) ($row->batch_year_number ?? 0); $batchLabel = trim((string) ($row->batch_title ?? '')); if ($batchLabel === '') { $fallbackNumber = $yearlyNumber > 0 ? $yearlyNumber : $batchId; $batchLabel = 'Batch #' . $fallbackNumber; } $adminId = $row->admin_id ? (int) $row->admin_id : 0; $adminName = $adminId > 0 ? (trim(($row->admin_firstname ?? '') . ' ' . ($row->admin_lastname ?? '')) ?: 'Admin') : 'Unassigned'; $batchStatus = strtolower(trim((string) ($row->batch_status ?? ''))) ?: 'open'; $item = [ 'id' => $expenseId, 'expense_id' => $expenseId, 'reimbursement_id' => $row->reimbursement_id ? (int) $row->reimbursement_id : null, 'expense_amount' => (float) ($row->expense_amount ?? 0), 'vendor' => (string) ($row->retailor ?? ''), 'description' => trim((string) ($row->description ?? '')), 'purchased_by' => $purchaser, 'receipt_url' => $receiptUrl, 'batch_id' => $batchId, 'batch_label' => $batchLabel, 'admin_id' => $adminId ?: null, 'admin_name' => $adminId > 0 ? $adminName : null, 'batch_number' => $batchId, ]; $itemsMap[$expenseId] = $item; if (!isset($batchStructures[$batchId])) { $batchStructures[$batchId] = [ 'batchId' => $batchId, 'label' => $batchLabel, 'slots' => [], 'status' => $batchStatus, 'yearly_batch_number' => $yearlyNumber, ]; } else { $batchStructures[$batchId]['status'] = $batchStatus; if ($yearlyNumber > 0) { $batchStructures[$batchId]['yearly_batch_number'] = $yearlyNumber; } } $slotKey = $adminId ?: 0; if (!isset($batchStructures[$batchId]['slots'][$slotKey])) { $batchStructures[$batchId]['slots'][$slotKey] = [ 'admin_id' => $adminId ?: 0, 'admin_name' => $adminId > 0 ? $adminName : 'Unassigned', 'items' => [], 'check_file' => null, ]; } $batchStructures[$batchId]['slots'][$slotKey]['items'][] = $expenseId; } $adminFileMap = []; if (!empty($batchStructures)) { $batchIds = array_keys($batchStructures); $files = ReimbursementBatchAdminFile::query() ->select('batch_id', 'admin_id', 'filename', 'original_filename') ->whereIn('batch_id', $batchIds) ->get(); foreach ($files as $file) { $batchId = (int) ($file->batch_id ?? 0); $adminId = (int) ($file->admin_id ?? 0); if ($batchId <= 0) { continue; } $adminFileMap[$batchId][$adminId] = [ 'filename' => $file->filename, 'original_filename' => $file->original_filename ?? null, 'url' => $this->files->adminFileUrl($file->filename ?? '', 'inline'), ]; } } foreach ($batchStructures as $batchId => &$batch) { foreach ($batch['slots'] as $slotKey => &$slot) { $adminId = $slot['admin_id'] ?? 0; $slot['check_file'] = $adminFileMap[$batchId][$adminId] ?? null; } unset($slot); } unset($batch); $existingBatches = array_map(static function (array $batch) { $normalizedSlots = array_map(static function (array $slot) { $slot['items'] = array_values(array_map('intval', $slot['items'])); return $slot; }, array_values($batch['slots'])); $itemIds = []; foreach ($normalizedSlots as $slot) { $itemIds = array_merge($itemIds, $slot['items']); } return [ 'batchId' => $batch['batchId'], 'batchNumber' => $batch['batchId'], 'label' => $batch['label'], 'sequence' => $batch['yearly_batch_number'] ?? 0, 'yearly_batch_number' => $batch['yearly_batch_number'] ?? 0, 'slots' => $normalizedSlots, 'itemIds' => array_values(array_unique($itemIds)), 'status' => $batch['status'] ?? 'open', ]; }, array_values($batchStructures)); $admins = $this->recipients->adminRoster(); return [ 'pendingItems' => $pendingItems, 'existingBatches' => $existingBatches, 'adminUsers' => $admins, 'itemsPayload' => array_values($itemsMap), ]; } public function index(array $filters): array { $expenses = Expense::getReimbursedExpensesWithDetails($filters) ->get() ->map(static fn ($row) => (array) $row) ->all(); $batchSummaries = []; $batchDetails = []; $batchAttachments = []; $batchQuery = DB::table('reimbursement_batches as b') ->selectRaw('b.id as batch_id, b.title as batch_title, b.yearly_batch_number, b.closed_at, b.school_year as batch_school_year, e.id as expense_id, e.amount as expense_amount, e.description, e.retailor, e.receipt_path as expense_receipt, e.date_of_purchase as expense_date_of_purchase, e.purchased_by, u.firstname as purchaser_firstname, u.lastname as purchaser_lastname, r.amount as reimb_amount') ->join('reimbursement_batch_items as bi', 'bi.batch_id', '=', 'b.id') ->join('expenses as e', 'e.id', '=', 'bi.expense_id') ->leftJoin('users as u', 'u.id', '=', 'e.purchased_by') ->leftJoin('reimbursements as r', 'r.expense_id', '=', 'e.id') ->where('b.status', 'closed') ->whereNull('bi.unassigned_at'); if (!empty($filters['school_year'])) { $batchQuery->where(function ($q) use ($filters) { $q->where('b.school_year', $filters['school_year']) ->orWhere('e.school_year', $filters['school_year']); }); } if (!empty($filters['user_id'])) { $batchQuery->where('e.purchased_by', $filters['user_id']); } $batchRows = $batchQuery->get(); foreach ($batchRows as $row) { $bid = (int) ($row->batch_id ?? 0); if ($bid <= 0) { continue; } $labelNumber = (int) ($row->yearly_batch_number ?? $bid); if (!isset($batchSummaries[$bid])) { $batchSummaries[$bid] = [ 'batch_id' => $bid, 'title' => trim((string) ($row->batch_title ?? '')) ?: ('Batch #' . $labelNumber), 'sequence' => $labelNumber, 'closed_at' => $row->closed_at ?? null, 'amount' => 0.0, 'items' => 0, ]; } $amount = $row->reimb_amount ?? $row->expense_amount ?? 0; $batchSummaries[$bid]['amount'] += (float) $amount; $batchSummaries[$bid]['items'] += 1; if (!isset($batchDetails[$bid])) { $batchDetails[$bid] = [ 'title' => $batchSummaries[$bid]['title'], 'sequence' => $labelNumber, 'closed_at' => $batchSummaries[$bid]['closed_at'], 'items' => [], 'checks' => [], ]; } if (!isset($batchAttachments[$bid])) { $batchAttachments[$bid] = [ 'receipts' => [], 'checks' => [], ]; } $batchDetails[$bid]['items'][] = [ 'amount' => (float) $amount, 'description' => $row->description ?? '', 'vendor' => $row->retailor ?? '', 'purchased_by' => trim(($row->purchaser_firstname ?? '') . ' ' . ($row->purchaser_lastname ?? '')), 'receipt_url' => $this->files->expenseReceiptUrl($row->expense_receipt ?? null), ]; $receiptFilename = trim((string) ($row->expense_receipt ?? '')); if ($receiptFilename !== '') { $batchAttachments[$bid]['receipts'][] = [ 'expense_id' => (int) ($row->expense_id ?? 0), 'receipt_filename' => $receiptFilename, 'receipt_url' => $this->files->expenseReceiptUrl($receiptFilename), 'purchaser_firstname' => $row->purchaser_firstname ?? '', 'purchaser_lastname' => $row->purchaser_lastname ?? '', 'vendor' => $row->retailor ?? '', 'description' => $row->description ?? '', 'amount' => (float) ($row->expense_amount ?? 0), 'date_of_purchase' => $row->expense_date_of_purchase ?? '', ]; } } if (!empty($batchSummaries)) { $batchIds = array_keys($batchSummaries); $files = ReimbursementBatchAdminFile::query() ->select('id as file_id', 'batch_id', 'admin_id', 'filename', 'original_filename', 'uploaded_at') ->whereIn('batch_id', $batchIds) ->get(); $adminNames = $this->recipients->adminRoster(); $adminNameMap = []; foreach ($adminNames as $admin) { $adminNameMap[(int) $admin['id']] = trim(($admin['firstname'] ?? '') . ' ' . ($admin['lastname'] ?? '')); } foreach ($files as $file) { $bid = (int) ($file->batch_id ?? 0); if ($bid <= 0 || !isset($batchDetails[$bid])) { continue; } $aid = (int) ($file->admin_id ?? 0); $batchDetails[$bid]['checks'][] = [ 'admin' => $adminNameMap[$aid] ?? ($aid > 0 ? 'Admin #' . $aid : 'Unassigned'), 'filename' => $file->filename, 'original' => $file->original_filename ?? $file->filename, 'url' => $this->files->adminFileUrl($file->filename ?? '', 'inline'), ]; if (!isset($batchAttachments[$bid])) { $batchAttachments[$bid] = [ 'receipts' => [], 'checks' => [], ]; } $batchAttachments[$bid]['checks'][] = [ 'file_id' => (int) ($file->file_id ?? 0), 'filename' => $file->filename, 'original_filename' => $file->original_filename ?? $file->filename, 'admin' => $adminNameMap[$aid] ?? ($aid > 0 ? 'Admin #' . $aid : 'Unassigned'), 'url' => $this->files->adminFileUrl($file->filename ?? '', 'inline'), 'uploaded_at' => $file->uploaded_at ?? null, ]; } } if (!empty($batchSummaries)) { $batchSummaries = array_values($batchSummaries); usort($batchSummaries, static function ($a, $b) { $aSeq = (int) ($a['sequence'] ?? 0); $bSeq = (int) ($b['sequence'] ?? 0); return $bSeq <=> $aSeq; }); } $donationBatch = null; $donationDetails = [ 'items' => [], 'checks' => [], ]; $donationQuery = DB::table('expenses as e') ->selectRaw('e.id as expense_id, e.amount as expense_amount, e.description, e.retailor, e.receipt_path as expense_receipt, e.purchased_by, e.school_year, e.semester, e.status, u.firstname as purchaser_firstname, u.lastname as purchaser_lastname') ->leftJoin('users as u', 'u.id', '=', 'e.purchased_by') ->where('e.category', 'Donation'); if (!empty($filters['school_year'])) { $donationQuery->where('e.school_year', $filters['school_year']); } if (!empty($filters['semester'])) { $donationQuery->where('e.semester', $filters['semester']); } if (!empty($filters['user_id'])) { $donationQuery->where('e.purchased_by', $filters['user_id']); } if (!empty($filters['status'])) { $donationQuery->where(function ($q) use ($filters) { $q->where('e.status', $filters['status']) ->orWhere('e.status', strtolower((string) $filters['status'])) ->orWhere('e.status', strtoupper((string) $filters['status'])); }); } $donationRows = $donationQuery->orderByDesc('e.created_at')->get(); if ($donationRows->isNotEmpty()) { $donationTotal = 0.0; foreach ($donationRows as $row) { $amount = (float) ($row->expense_amount ?? 0); $donationTotal += $amount; $purchaser = trim(($row->purchaser_firstname ?? '') . ' ' . ($row->purchaser_lastname ?? '')); if ($purchaser === '') { $purchaser = 'Unknown'; } $donationDetails['items'][] = [ 'amount' => $amount, 'description' => $row->description ?? '', 'vendor' => $row->retailor ?? '', 'purchased_by' => $purchaser, 'receipt_url' => $this->files->expenseReceiptUrl($row->expense_receipt ?? null), ]; } $donationBatch = [ 'title' => 'Donations', 'items' => count($donationDetails['items']), 'amount' => $donationTotal, ]; } foreach ($expenses as &$e) { if (array_key_exists('expense_receipt', $e)) { $e['expense_receipt_url'] = $this->files->expenseReceiptUrl($e['expense_receipt']); } elseif (array_key_exists('receipt_path', $e)) { $e['expense_receipt_url'] = $this->files->expenseReceiptUrl($e['receipt_path']); } if (array_key_exists('reimb_receipt', $e)) { $e['reimb_receipt_url'] = $this->files->receiptUrl($e['reimb_receipt']); } elseif (array_key_exists('receipt_path_reimb', $e)) { $e['reimb_receipt_url'] = $this->files->receiptUrl($e['receipt_path_reimb']); } $recipientId = $e['reimb_recipient_id'] ?? ($e['reimbursed_to'] ?? null); $hasName = trim(($e['reimb_firstname'] ?? '') . ' ' . ($e['reimb_lastname'] ?? '')) !== ''; if (!$hasName) { $label = $this->recipients->specialRecipientLabel($recipientId); if ($label !== null) { $e['reimb_firstname'] = $label; $e['reimb_lastname'] = ''; } } } unset($e); $users = $this->recipients->recipientOptions(); $years = DB::table('reimbursements') ->select('school_year') ->distinct() ->orderByDesc('school_year') ->get(); $schoolYears = array_values(array_filter(array_map(static fn ($row) => $row->school_year ?? null, $years->all()))); return [ 'expenses' => $expenses, 'users' => $users, 'schoolYears' => $schoolYears, 'batchSummaries' => $batchSummaries, 'batchDetails' => $batchDetails, 'batchAttachments' => $batchAttachments, 'donationBatch' => $donationBatch, 'donationDetails' => $donationDetails, ]; } public function reimbursedExpenses(): array { $rows = DB::table('reimbursements as r') ->selectRaw('e.amount as expense_amount, e.category, e.description, e.receipt_path as expense_receipt, e.purchased_by, r.amount as reimb_amount, r.reimbursement_method, r.check_number, r.receipt_path as reimb_receipt, r.status as reimb_status, r.created_at, u.firstname as purchaser_firstname, u.lastname as purchaser_lastname, a.firstname as approver_firstname, a.lastname as approver_lastname') ->join('expenses as e', 'e.id', '=', 'r.expense_id') ->leftJoin('users as u', 'u.id', '=', 'e.purchased_by') ->leftJoin('users as a', 'a.id', '=', 'r.approved_by') ->orderByDesc('r.created_at') ->get(); $expenses = []; foreach ($rows as $row) { $expenses[] = [ 'expense_amount' => (float) ($row->expense_amount ?? 0), 'category' => $row->category ?? null, 'description' => $row->description ?? null, 'expense_receipt' => $row->expense_receipt ?? null, 'purchased_by' => $row->purchased_by ?? null, 'reimb_amount' => (float) ($row->reimb_amount ?? 0), 'reimbursement_method' => $row->reimbursement_method ?? null, 'check_number' => $row->check_number ?? null, 'reimb_receipt' => $row->reimb_receipt ?? null, 'reimb_status' => $row->reimb_status ?? null, 'created_at' => $row->created_at ?? null, 'purchaser_firstname' => $row->purchaser_firstname ?? null, 'purchaser_lastname' => $row->purchaser_lastname ?? null, 'approver_firstname' => $row->approver_firstname ?? null, 'approver_lastname' => $row->approver_lastname ?? null, 'expense_receipt_url' => $this->files->expenseReceiptUrl($row->expense_receipt ?? null), 'reimb_receipt_url' => $this->files->receiptUrl($row->reimb_receipt ?? null), ]; } return $expenses; } }