499 lines
22 KiB
PHP
499 lines
22 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Reimbursements;
|
|
|
|
use App\Models\Expense;
|
|
use App\Models\ReimbursementBatchAdminFile;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class ReimbursementQueryService
|
|
{
|
|
public function __construct(
|
|
private ReimbursementRecipientService $recipients,
|
|
private ReimbursementFileService $files
|
|
) {
|
|
}
|
|
|
|
public function underProcessing(): array
|
|
{
|
|
$pendingQuery = DB::table('expenses as e')
|
|
->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;
|
|
}
|
|
}
|