add projet
This commit is contained in:
+429
@@ -0,0 +1,429 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use App\Models\Expense;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ExpenseController extends BaseApiController
|
||||
{
|
||||
protected Expense $expense;
|
||||
protected User $user;
|
||||
protected Configuration $config;
|
||||
protected ?string $schoolYear;
|
||||
protected ?string $semester;
|
||||
protected array $retailors;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->expense = model(Expense::class);
|
||||
$this->user = model(User::class);
|
||||
$this->config = model(Configuration::class);
|
||||
|
||||
$this->schoolYear = $this->config->getConfig('school_year');
|
||||
$this->semester = $this->config->getConfig('semester');
|
||||
|
||||
// Default list of common retailors; adjust as needed
|
||||
$this->retailors = [
|
||||
'Amazon',
|
||||
'Walmart',
|
||||
'Costco',
|
||||
'BJ\'s',
|
||||
'Market Basket',
|
||||
'Aldi',
|
||||
'Hannaford',
|
||||
'Sam\'s Club',
|
||||
'HomeGoods',
|
||||
'Hostinger',
|
||||
'Wicked Cheesy',
|
||||
'Shatila',
|
||||
'Brothers Pizzeria',
|
||||
'Paradise Biryani Pointe',
|
||||
'Emad Leiman',
|
||||
'Nova Trampoline Park',
|
||||
'Lubin\'s Awards',
|
||||
'Dollar Tree',
|
||||
'Stop & Shop',
|
||||
'Dunkin\' Donuts',
|
||||
'Giovanni\'s Pizza'
|
||||
];
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$page = max(1, (int) ($this->request->getGet('page') ?? 1));
|
||||
$perPage = min(100, max(1, (int) ($this->request->getGet('per_page') ?? 20)));
|
||||
$status = $this->request->getGet('status');
|
||||
$category = $this->request->getGet('category');
|
||||
|
||||
$query = $this->expense
|
||||
->select("
|
||||
expenses.*,
|
||||
u.firstname AS purchaser_firstname, u.lastname AS purchaser_lastname,
|
||||
approver.firstname AS approver_firstname, approver.lastname AS approver_lastname
|
||||
")
|
||||
->join('users u', 'u.id = expenses.purchased_by', 'left')
|
||||
->join('users approver', 'approver.id = expenses.approved_by', 'left');
|
||||
|
||||
if ($this->schoolYear) {
|
||||
$query->where('expenses.school_year', $this->schoolYear);
|
||||
}
|
||||
if ($this->semester) {
|
||||
$query->where('expenses.semester', $this->semester);
|
||||
}
|
||||
if ($status) {
|
||||
$query->where('expenses.status', $status);
|
||||
}
|
||||
if ($category) {
|
||||
$query->where('expenses.category', $category);
|
||||
}
|
||||
|
||||
$expenses = $query->orderBy('expenses.created_at', 'DESC')->findAll();
|
||||
|
||||
// Enrich each row with a URL that goes through Files::receipt($name)
|
||||
$expenses = array_map(function ($row) {
|
||||
$name = $row['receipt_path'] ?? null;
|
||||
$row['receipt_url'] = $this->receiptUrl($name);
|
||||
return $row;
|
||||
}, $expenses);
|
||||
|
||||
// Apply pagination manually for array results
|
||||
$total = count($expenses);
|
||||
$offset = ($page - 1) * $perPage;
|
||||
$paginatedExpenses = array_slice($expenses, $offset, $perPage);
|
||||
|
||||
$result = [
|
||||
'data' => $paginatedExpenses,
|
||||
'pagination' => [
|
||||
'current_page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total' => $total,
|
||||
'total_pages' => (int) ceil($total / $perPage),
|
||||
],
|
||||
];
|
||||
|
||||
return $this->success($result, 'Expenses retrieved successfully');
|
||||
}
|
||||
|
||||
public function show($id = null)
|
||||
{
|
||||
$expense = $this->expense
|
||||
->select("
|
||||
expenses.*,
|
||||
u.firstname AS purchaser_firstname, u.lastname AS purchaser_lastname,
|
||||
approver.firstname AS approver_firstname, approver.lastname AS approver_lastname
|
||||
")
|
||||
->join('users u', 'u.id = expenses.purchased_by', 'left')
|
||||
->join('users approver', 'approver.id = expenses.approved_by', 'left')
|
||||
->where('expenses.id', $id)
|
||||
->first();
|
||||
|
||||
if (!$expense) {
|
||||
return $this->respondError('Expense not found', Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Add receipt URL
|
||||
$expense['receipt_url'] = $this->receiptUrl($expense['receipt_path'] ?? null);
|
||||
|
||||
return $this->success($expense, 'Expense retrieved successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata for creating an expense (users list and retailors)
|
||||
*/
|
||||
public function createMetadata()
|
||||
{
|
||||
// Limit users to non-teacher/non-parent
|
||||
$users = $this->user
|
||||
->select('users.id, users.firstname, users.lastname')
|
||||
->join('user_roles', 'user_roles.user_id = users.id')
|
||||
->join('roles', 'roles.id = user_roles.role_id')
|
||||
->whereNotIn('roles.name', ['teacher', 'parent'])
|
||||
->distinct()
|
||||
->findAll();
|
||||
|
||||
return $this->success([
|
||||
'users' => $users,
|
||||
'retailors' => $this->retailors,
|
||||
], 'Metadata retrieved successfully');
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user) {
|
||||
return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$data = $this->payloadData();
|
||||
if (empty($data)) {
|
||||
return $this->respondError('Invalid request data', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'category' => 'required|in:Expense,Purchase,Reimbursement',
|
||||
'amount' => 'required|numeric|gt:0',
|
||||
'purchased_by' => 'required',
|
||||
'retailor' => 'nullable|max:255',
|
||||
'date_of_purchase'=> 'nullable|date',
|
||||
];
|
||||
|
||||
// Handle file upload validation
|
||||
if ($this->laravelRequest->hasFile('receipt')) {
|
||||
$rules['receipt'] = 'file|max:2048|mimes:jpg,jpeg,png,webp,gif,pdf';
|
||||
} else {
|
||||
$rules['receipt'] = 'required|file|max:2048|mimes:jpg,jpeg,png,webp,gif,pdf';
|
||||
}
|
||||
|
||||
// Use Laravel Validator directly for file validation
|
||||
$validator = Validator::make($this->laravelRequest->all(), $rules);
|
||||
if ($validator->fails()) {
|
||||
return $this->respondValidationError($validator->errors()->toArray());
|
||||
}
|
||||
|
||||
// Parse "purchased_by" as "7|John Doe" if provided in that format
|
||||
$purchasedById = $user->id;
|
||||
if (isset($data['purchased_by'])) {
|
||||
$purchasedInfo = (string) $data['purchased_by'];
|
||||
if (strpos($purchasedInfo, '|') !== false) {
|
||||
[$purchasedById, $purchasedByName] = array_pad(explode('|', $purchasedInfo, 2), 2, null);
|
||||
$purchasedById = (int) $purchasedById;
|
||||
} else {
|
||||
$purchasedById = (int) $purchasedInfo;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle file upload
|
||||
$receiptName = null;
|
||||
if ($this->laravelRequest->hasFile('receipt')) {
|
||||
$file = $this->laravelRequest->file('receipt');
|
||||
if ($file->isValid()) {
|
||||
$targetDir = storage_path('app/uploads/receipts');
|
||||
if (!is_dir($targetDir)) {
|
||||
@mkdir($targetDir, 0755, true);
|
||||
}
|
||||
$extension = $file->getClientOriginalExtension();
|
||||
$newName = uniqid('', true) . '.' . $extension;
|
||||
$file->move($targetDir, $newName);
|
||||
$receiptName = $newName;
|
||||
}
|
||||
}
|
||||
|
||||
$schoolYear = $this->schoolYear ?: date('Y');
|
||||
$semester = $this->semester ?: 'Fall';
|
||||
|
||||
$expenseData = [
|
||||
'category' => $data['category'],
|
||||
'amount' => $data['amount'],
|
||||
'receipt_path' => $receiptName,
|
||||
'description' => $data['description'] ?? null,
|
||||
'retailor' => !empty($data['retailor']) ? trim($data['retailor']) : null,
|
||||
'date_of_purchase' => $data['date_of_purchase'] ?? null,
|
||||
'purchased_by' => $purchasedById,
|
||||
'added_by' => $user->id,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'status' => 'pending',
|
||||
];
|
||||
|
||||
try {
|
||||
$expense = $this->expense->create($expenseData);
|
||||
$expense['receipt_url'] = $this->receiptUrl($receiptName);
|
||||
return $this->respondCreated($expense, 'Expense created successfully');
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Expense creation error: ' . $e->getMessage());
|
||||
return $this->respondError('Failed to create expense', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata for editing an expense
|
||||
*/
|
||||
public function editMetadata($id = null)
|
||||
{
|
||||
$expense = $this->expense->find($id);
|
||||
if (!$expense) {
|
||||
return $this->respondError('Expense not found', Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Get users list
|
||||
$users = $this->user
|
||||
->select('users.id, users.firstname, users.lastname')
|
||||
->join('user_roles', 'user_roles.user_id = users.id')
|
||||
->join('roles', 'roles.id = user_roles.role_id')
|
||||
->whereNotIn('roles.name', ['teacher', 'parent'])
|
||||
->distinct()
|
||||
->findAll();
|
||||
|
||||
$expense['receipt_url'] = $expense['receipt_path'] ? $this->receiptUrl(basename($expense['receipt_path'])) : null;
|
||||
|
||||
return $this->success([
|
||||
'expense' => $expense,
|
||||
'users' => $users,
|
||||
'retailors' => $this->retailors,
|
||||
], 'Metadata retrieved successfully');
|
||||
}
|
||||
|
||||
public function update($id = null)
|
||||
{
|
||||
$expense = $this->expense->find($id);
|
||||
if (!$expense) {
|
||||
return $this->respondError('Expense not found', Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user) {
|
||||
return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$data = $this->payloadData();
|
||||
if (empty($data)) {
|
||||
return $this->respondError('Invalid request data', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'category' => 'required|in:Expense,Purchase,Reimbursement',
|
||||
'amount' => 'required|numeric|gt:0',
|
||||
'purchased_by' => 'required',
|
||||
'retailor' => 'nullable|max:255',
|
||||
'date_of_purchase'=> 'nullable|date',
|
||||
];
|
||||
|
||||
// Optional new receipt validation (only if provided)
|
||||
if ($this->laravelRequest->hasFile('receipt')) {
|
||||
$rules['receipt'] = 'file|max:2048|mimes:jpg,jpeg,png,webp,gif,pdf';
|
||||
}
|
||||
|
||||
// Use Laravel Validator directly for file validation
|
||||
$validator = Validator::make($this->laravelRequest->all(), $rules);
|
||||
if ($validator->fails()) {
|
||||
return $this->respondValidationError($validator->errors()->toArray());
|
||||
}
|
||||
|
||||
// Parse "purchased_by" as "id|Name" if provided in that format
|
||||
$purchasedById = $expense['purchased_by'];
|
||||
if (isset($data['purchased_by'])) {
|
||||
$purchasedInfo = (string) $data['purchased_by'];
|
||||
if (strpos($purchasedInfo, '|') !== false) {
|
||||
[$purchasedById] = array_pad(explode('|', $purchasedInfo, 2), 2, null);
|
||||
$purchasedById = (int) $purchasedById;
|
||||
} else {
|
||||
$purchasedById = (int) $purchasedInfo;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep old receipt unless replaced or removed
|
||||
$receiptName = $expense['receipt_path'];
|
||||
if ($this->laravelRequest->hasFile('receipt')) {
|
||||
$file = $this->laravelRequest->file('receipt');
|
||||
if ($file->isValid()) {
|
||||
$targetDir = storage_path('app/uploads/receipts');
|
||||
if (!is_dir($targetDir)) {
|
||||
@mkdir($targetDir, 0755, true);
|
||||
}
|
||||
$extension = $file->getClientOriginalExtension();
|
||||
$newName = uniqid('', true) . '.' . $extension;
|
||||
$file->move($targetDir, $newName);
|
||||
$receiptName = $newName;
|
||||
}
|
||||
}
|
||||
if (isset($data['remove_receipt']) && $data['remove_receipt'] === '1') {
|
||||
$receiptName = null;
|
||||
}
|
||||
|
||||
$updateData = [
|
||||
'category' => $data['category'],
|
||||
'amount' => $data['amount'],
|
||||
'description' => $data['description'] ?? $expense['description'],
|
||||
'retailor' => isset($data['retailor']) ? (trim($data['retailor']) ?: null) : $expense['retailor'],
|
||||
'date_of_purchase'=> $data['date_of_purchase'] ?? $expense['date_of_purchase'],
|
||||
'purchased_by' => $purchasedById,
|
||||
'receipt_path' => $receiptName,
|
||||
'updated_by' => $user->id,
|
||||
];
|
||||
|
||||
try {
|
||||
$this->expense->update($id, $updateData);
|
||||
$updatedExpense = $this->expense->find($id);
|
||||
$updatedExpense['receipt_url'] = $this->receiptUrl($receiptName);
|
||||
return $this->success($updatedExpense, 'Expense updated successfully');
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Expense update error: ' . $e->getMessage());
|
||||
return $this->respondError('Failed to update expense', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
public function updateStatus()
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user) {
|
||||
return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$data = $this->payloadData();
|
||||
$id = isset($data['id']) ? (int)$data['id'] : null;
|
||||
$status = $data['status'] ?? null;
|
||||
$reason = $data['reason'] ?? '';
|
||||
|
||||
if (!$id || !in_array($status, ['approved', 'denied'], true)) {
|
||||
log_message('error', 'Invalid status or ID');
|
||||
return $this->respondError('Invalid data', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$expense = $this->expense->find($id);
|
||||
if (!$expense) {
|
||||
log_message('error', 'Expense not found for ID ' . $id);
|
||||
return $this->respondError('Expense not found', Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->expense->update($id, [
|
||||
'status' => $status,
|
||||
'status_reason' => $reason,
|
||||
'approved_by' => $user->id,
|
||||
'updated_by' => $user->id
|
||||
]);
|
||||
|
||||
$updatedExpense = $this->expense->find($id);
|
||||
return $this->success($updatedExpense, 'Expense status updated successfully');
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Expense update failed for ID ' . $id . ': ' . $e->getMessage());
|
||||
return $this->respondError('Update failed', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($id = null)
|
||||
{
|
||||
$expense = $this->expense->find($id);
|
||||
if (!$expense) {
|
||||
return $this->respondError('Expense not found', Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$expense->delete();
|
||||
return $this->success(null, 'Expense deleted successfully');
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Expense delete error: ' . $e->getMessage());
|
||||
return $this->respondError('Failed to delete expense', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a public URL for a receipt filename through Files::receipt($name).
|
||||
* Expects just the filename (e.g., "1759113425_1c443e607e1900f92f62.png").
|
||||
*/
|
||||
private function receiptUrl(?string $filename): ?string
|
||||
{
|
||||
if (!$filename) {
|
||||
return null;
|
||||
}
|
||||
// Using API route pattern - ensure route exists in routes/api.php
|
||||
// Example: Route::get('files/receipt/{name}', [FilesController::class, 'receipt']);
|
||||
return url('/api/v1/files/receipt/' . $filename);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user