initial: bootstrap from BukidBountyApp base
This commit is contained in:
751
app/Http/Controllers/Accounting/AccountingController.php
Normal file
751
app/Http/Controllers/Accounting/AccountingController.php
Normal file
@@ -0,0 +1,751 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Accounting;
|
||||
|
||||
use App\Http\Controllers\AbstractController;
|
||||
use App\Models\Accounting\Account;
|
||||
use App\Models\Accounting\AccountTransaction;
|
||||
use App\Models\Market\Store;
|
||||
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
|
||||
use App\Enums\UserActions;
|
||||
use App\Enums\UserTypes;
|
||||
use App\Http\Controllers\Helpers\ResponseHelper;
|
||||
use App\Support\ModuleHelper;
|
||||
use Hypervel\Http\Request;
|
||||
use Hypervel\Support\Facades\Auth;
|
||||
use Hypervel\Support\Facades\DB;
|
||||
use Hyperf\Stringable\Str;
|
||||
|
||||
class AccountingController extends AbstractController
|
||||
{
|
||||
// ─── helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
private function resolveType(): UserTypes
|
||||
{
|
||||
$user = Auth::user();
|
||||
return $user->acct_type instanceof UserTypes
|
||||
? $user->acct_type
|
||||
: (UserTypes::tryFrom($user->acct_type) ?? UserTypes::PUBLIC);
|
||||
}
|
||||
|
||||
private function isBig3(UserTypes $t): bool
|
||||
{
|
||||
return in_array($t, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR], true);
|
||||
}
|
||||
|
||||
private function isStoreLevel(UserTypes $t): bool
|
||||
{
|
||||
return in_array($t, [UserTypes::STORE_OWNER, UserTypes::STORE_MANAGER], true);
|
||||
}
|
||||
|
||||
/** IDs of stores the current user owns or manages (including descendants). */
|
||||
private function userStoreIds(): array
|
||||
{
|
||||
$user = Auth::user();
|
||||
$allowedUserIds = array_merge([$user->id], $user->getAllDescendants()->pluck('id')->toArray());
|
||||
|
||||
return Store::where(function ($q) use ($allowedUserIds) {
|
||||
$q->whereIn('owner_id', $allowedUserIds)
|
||||
->orWhereIn('manager_id', $allowedUserIds)
|
||||
->orWhereHas('managers', function ($mq) use ($allowedUserIds) {
|
||||
$mq->whereIn('user_id', $allowedUserIds);
|
||||
});
|
||||
})->pluck('id')->toArray();
|
||||
}
|
||||
|
||||
/** Apply store_id scope to a query based on the caller's role. */
|
||||
private function scopeAccounts($query, array $storeIds, bool $isBig3)
|
||||
{
|
||||
if ($isBig3) {
|
||||
return $query->whereNull('store_id');
|
||||
}
|
||||
return $query->whereIn('store_id', $storeIds);
|
||||
}
|
||||
|
||||
/** Check store-level access. Returns false if the accounting_store module is disabled. */
|
||||
private function guardStore(): bool
|
||||
{
|
||||
return ModuleHelper::isEnabled('accounting_store');
|
||||
}
|
||||
|
||||
// ─── public endpoints ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Full Chart of Accounts tree.
|
||||
*/
|
||||
public function getAccountsTree(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$type = $this->resolveType();
|
||||
$isBig3 = $this->isBig3($type);
|
||||
|
||||
if (!$isBig3 && !$this->guardStore()) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$storeIds = $isBig3 ? [] : $this->userStoreIds();
|
||||
|
||||
$base = Account::whereNull('parent_id')->where('is_active', true);
|
||||
$base = $this->scopeAccounts($base, $storeIds, $isBig3);
|
||||
|
||||
$accounts = $base
|
||||
->with(['children' => function ($q) use ($storeIds, $isBig3) {
|
||||
$q = $q->where('is_active', true);
|
||||
$q = $this->scopeAccounts($q, $storeIds, $isBig3);
|
||||
$q->with(['children' => function ($q2) use ($storeIds, $isBig3) {
|
||||
$q2 = $q2->where('is_active', true);
|
||||
$q2 = $this->scopeAccounts($q2, $storeIds, $isBig3);
|
||||
}]);
|
||||
}])
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
return response()->json(['success' => true, 'data' => $accounts]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Leaf accounts for the Daily Entry form.
|
||||
*/
|
||||
public function getLeafAccounts(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$type = $this->resolveType();
|
||||
$isBig3 = $this->isBig3($type);
|
||||
|
||||
if (!$isBig3 && !$this->guardStore()) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$storeIds = $isBig3 ? [] : $this->userStoreIds();
|
||||
|
||||
$base = Account::where('is_active', true)->whereDoesntHave('children');
|
||||
$base = $this->scopeAccounts($base, $storeIds, $isBig3);
|
||||
|
||||
$leafAccounts = $base
|
||||
->with('parent.parent')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
return response()->json(['success' => true, 'data' => $leafAccounts]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Daily transactions for a given date.
|
||||
*/
|
||||
public function getDailyTransactions(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$type = $this->resolveType();
|
||||
$isBig3 = $this->isBig3($type);
|
||||
|
||||
if (!$isBig3 && !$this->guardStore()) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$storeIds = $isBig3 ? [] : $this->userStoreIds();
|
||||
$date = $request->input('date', date('Y-m-d'));
|
||||
|
||||
$accountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id');
|
||||
|
||||
$transactions = AccountTransaction::with(['account'])
|
||||
->whereIn('account_id', $accountIds)
|
||||
->whereDate('transaction_date', $date)
|
||||
->get();
|
||||
|
||||
$mapped = [];
|
||||
foreach ($transactions as $txn) {
|
||||
$mapped[$txn->account_id] = [
|
||||
'id' => $txn->id,
|
||||
'amount' => $txn->amount,
|
||||
'notes' => $txn->notes,
|
||||
];
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'date' => $date, 'data' => $mapped]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk save daily transactions.
|
||||
*/
|
||||
public function saveDailyTransactions(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$type = $this->resolveType();
|
||||
$isBig3 = $this->isBig3($type);
|
||||
|
||||
if (!$isBig3 && !$this->guardStore()) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$storeIds = $isBig3 ? [] : $this->userStoreIds();
|
||||
$date = $request->input('date');
|
||||
$entries = $request->input('entries', []);
|
||||
$userId = Auth::id();
|
||||
|
||||
// Allowed account IDs for this user (prevent writing to other stores' accounts)
|
||||
$allowedAccountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id')->flip()->all();
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
foreach ($entries as $accountId => $entryData) {
|
||||
if (!array_key_exists($accountId, $allowedAccountIds)) {
|
||||
continue; // skip accounts not owned by this user
|
||||
}
|
||||
|
||||
$amount = (float)($entryData['amount'] ?? 0);
|
||||
$account = Account::find($accountId);
|
||||
if (!$account) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$flow = $account->default_flow
|
||||
?: ((strtoupper((string) $account->type) === 'REVENUE' || strtoupper((string) $account->type) === 'LIABILITY') ? 'INCOME' : 'EXPENSE');
|
||||
|
||||
$txn = AccountTransaction::where('account_id', $accountId)
|
||||
->whereDate('transaction_date', $date)
|
||||
->first();
|
||||
|
||||
if ($amount == 0) {
|
||||
if ($txn) {
|
||||
$txn->delete();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($txn) {
|
||||
$txn->amount = $amount;
|
||||
$txn->notes = $entryData['notes'] ?? null;
|
||||
$txn->updated_by = $userId;
|
||||
$txn->save();
|
||||
} else {
|
||||
AccountTransaction::create([
|
||||
'hashkey' => Str::random(32),
|
||||
'account_id' => $accountId,
|
||||
'amount' => $amount,
|
||||
'flow' => $flow,
|
||||
'transaction_date' => $date,
|
||||
'notes' => $entryData['notes'] ?? null,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
DB::commit();
|
||||
return response()->json(['success' => true, 'message' => 'Daily transactions saved.']);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return response()->json(['success' => false, 'message' => 'Error saving: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction list for CRUD tab.
|
||||
*/
|
||||
public function listTransactions(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$type = $this->resolveType();
|
||||
$isBig3 = $this->isBig3($type);
|
||||
|
||||
if (!$isBig3 && !$this->guardStore()) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$storeIds = $isBig3 ? [] : $this->userStoreIds();
|
||||
$accountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id');
|
||||
|
||||
$query = AccountTransaction::with(['account', 'creator', 'updater'])
|
||||
->whereIn('account_id', $accountIds);
|
||||
|
||||
if ($request->filled('account_id')) {
|
||||
$query->where('account_id', $request->account_id);
|
||||
}
|
||||
if ($request->filled('date_from')) {
|
||||
$query->whereDate('transaction_date', '>=', $request->date_from);
|
||||
}
|
||||
if ($request->filled('date_to')) {
|
||||
$query->whereDate('transaction_date', '<=', $request->date_to);
|
||||
}
|
||||
|
||||
$transactions = $query->orderBy('transaction_date', 'desc')->paginate(50);
|
||||
|
||||
return response()->json(['success' => true, 'data' => $transactions]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete single transaction.
|
||||
*/
|
||||
public function deleteTransaction(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$type = $this->resolveType();
|
||||
$isBig3 = $this->isBig3($type);
|
||||
$storeIds = $isBig3 ? [] : $this->userStoreIds();
|
||||
$allowedAccountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id')->all();
|
||||
|
||||
$txn = AccountTransaction::find($request->id);
|
||||
if ($txn && in_array($txn->account_id, $allowedAccountIds)) {
|
||||
$txn->delete();
|
||||
}
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Monthly matrix report (Rows = Days, Columns = Accounts).
|
||||
*/
|
||||
public function getMonthlyReport(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$type = $this->resolveType();
|
||||
$isBig3 = $this->isBig3($type);
|
||||
|
||||
if (!$isBig3 && !$this->guardStore()) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$storeIds = $isBig3 ? [] : $this->userStoreIds();
|
||||
$year = $request->input('year', date('Y'));
|
||||
$month = $request->input('month', date('m'));
|
||||
|
||||
$accountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id')->all();
|
||||
|
||||
$transactions = AccountTransaction::whereYear('transaction_date', $year)
|
||||
->whereMonth('transaction_date', $month)
|
||||
->whereIn('account_id', $accountIds)
|
||||
->get();
|
||||
|
||||
$daysInMonth = cal_days_in_month(CAL_GREGORIAN, (int)$month, (int)$year);
|
||||
$matrix = [];
|
||||
for ($i = 1; $i <= $daysInMonth; $i++) {
|
||||
$matrix[$i] = [];
|
||||
}
|
||||
|
||||
foreach ($transactions as $txn) {
|
||||
$day = (int)date('d', strtotime($txn->transaction_date));
|
||||
$matrix[$day][$txn->account_id] = ($matrix[$day][$txn->account_id] ?? 0) + $txn->amount;
|
||||
}
|
||||
|
||||
$columnTotals = [];
|
||||
foreach ($transactions as $txn) {
|
||||
$columnTotals[$txn->account_id] = ($columnTotals[$txn->account_id] ?? 0) + $txn->amount;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'days_in_month' => $daysInMonth,
|
||||
'matrix' => $matrix,
|
||||
'column_totals' => $columnTotals,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new account node. For store-level users the account is
|
||||
* automatically scoped to their store (first owned store).
|
||||
*/
|
||||
public function createAccount(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$type = $this->resolveType();
|
||||
$isBig3 = $this->isBig3($type);
|
||||
|
||||
if (!$isBig3 && !$this->guardStore()) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$name = trim((string) $request->input('name', ''));
|
||||
$accountType = strtoupper(trim((string) $request->input('type', '')));
|
||||
$defaultFlow = strtoupper(trim((string) $request->input('default_flow', '')));
|
||||
$parentId = $request->input('parent_id') ?: null;
|
||||
$description = $request->input('description') ?: null;
|
||||
|
||||
if ($name === '' || $accountType === '') {
|
||||
return response()->json(['success' => false, 'message' => 'Name and type are required.'], 422);
|
||||
}
|
||||
if (!in_array($defaultFlow, ['INCOME', 'EXPENSE'], true)) {
|
||||
$defaultFlow = in_array($accountType, ['REVENUE', 'LIABILITY']) ? 'INCOME' : 'EXPENSE';
|
||||
}
|
||||
if ($parentId !== null && !Account::where('id', $parentId)->exists()) {
|
||||
return response()->json(['success' => false, 'message' => 'Parent account not found.'], 422);
|
||||
}
|
||||
|
||||
// Determine store_id: null for Big3 (global), first store for store-level users
|
||||
$storeId = null;
|
||||
if (!$isBig3) {
|
||||
$storeIds = $this->userStoreIds();
|
||||
if (empty($storeIds)) {
|
||||
return response()->json(['success' => false, 'message' => 'No store found for your account. Create a store first.'], 422);
|
||||
}
|
||||
// If a parent is supplied, inherit its store_id; otherwise use the request-supplied store_id or the user's first store
|
||||
if ($parentId) {
|
||||
$parent = Account::find($parentId);
|
||||
$storeId = $parent?->store_id ?? $storeIds[0];
|
||||
} else {
|
||||
$requestedStoreId = $request->input('store_id');
|
||||
$storeId = ($requestedStoreId && in_array($requestedStoreId, $storeIds))
|
||||
? (int)$requestedStoreId
|
||||
: $storeIds[0];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$account = Account::create([
|
||||
'hashkey' => Str::random(40),
|
||||
'parent_id' => $parentId,
|
||||
'store_id' => $storeId,
|
||||
'type' => $accountType,
|
||||
'default_flow' => $defaultFlow,
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'is_active' => true,
|
||||
'created_by' => Auth::id(),
|
||||
'updated_by' => Auth::id(),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
\Hypervel\Support\Facades\Log::error('createAccount failed', [
|
||||
'message' => $e->getMessage(),
|
||||
'name' => $name,
|
||||
'type' => $accountType,
|
||||
'parent_id' => $parentId,
|
||||
'store_id' => $storeId,
|
||||
'user_id' => Auth::id(),
|
||||
]);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Could not create account: ' . $e->getMessage(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'data' => $account]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update editable fields (name, type, default_flow, description).
|
||||
*/
|
||||
public function updateAccount(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$account = Account::find($request->input('id'));
|
||||
if (!$account) {
|
||||
return response()->json(['success' => false, 'message' => 'Account not found.'], 404);
|
||||
}
|
||||
|
||||
// Store-level users may only edit their own store's accounts
|
||||
$type = $this->resolveType();
|
||||
$isBig3 = $this->isBig3($type);
|
||||
if (!$isBig3) {
|
||||
if (!$this->guardStore()) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
$storeIds = $this->userStoreIds();
|
||||
if (!in_array($account->store_id, $storeIds)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$account->name = trim((string) $request->input('name'));
|
||||
}
|
||||
if ($request->filled('type')) {
|
||||
$account->type = strtoupper(trim((string) $request->input('type')));
|
||||
}
|
||||
if ($request->filled('default_flow')) {
|
||||
$flow = strtoupper(trim((string) $request->input('default_flow')));
|
||||
if (!in_array($flow, ['INCOME', 'EXPENSE'], true)) {
|
||||
return response()->json(['success' => false, 'message' => 'default_flow must be INCOME or EXPENSE.'], 422);
|
||||
}
|
||||
$account->default_flow = $flow;
|
||||
}
|
||||
if ($request->has('description')) {
|
||||
$account->description = $request->input('description') ?: null;
|
||||
}
|
||||
|
||||
$account->updated_by = Auth::id();
|
||||
$account->save();
|
||||
|
||||
return response()->json(['success' => true, 'data' => $account]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive (soft-disable) an account.
|
||||
*/
|
||||
public function archiveAccount(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$account = Account::find($request->input('id'));
|
||||
if (!$account) {
|
||||
return response()->json(['success' => false, 'message' => 'Account not found.'], 404);
|
||||
}
|
||||
|
||||
$type = $this->resolveType();
|
||||
$isBig3 = $this->isBig3($type);
|
||||
if (!$isBig3) {
|
||||
if (!$this->guardStore()) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
$storeIds = $this->userStoreIds();
|
||||
if (!in_array($account->store_id, $storeIds)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
}
|
||||
|
||||
if (Account::where('parent_id', $account->id)->where('is_active', true)->exists()) {
|
||||
return response()->json(['success' => false, 'message' => 'Archive child accounts first.'], 422);
|
||||
}
|
||||
if (AccountTransaction::where('account_id', $account->id)->exists()) {
|
||||
return response()->json(['success' => false, 'message' => 'Cannot archive: account has transactions. Move or delete them first.'], 422);
|
||||
}
|
||||
|
||||
$account->is_active = false;
|
||||
$account->updated_by = Auth::id();
|
||||
$account->save();
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an account under a new parent (or to root). Refuses cycles.
|
||||
*/
|
||||
public function moveAccount(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$account = Account::find($request->input('id'));
|
||||
if (!$account) {
|
||||
return response()->json(['success' => false, 'message' => 'Account not found.'], 404);
|
||||
}
|
||||
|
||||
$type = $this->resolveType();
|
||||
$isBig3 = $this->isBig3($type);
|
||||
if (!$isBig3) {
|
||||
if (!$this->guardStore()) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
$storeIds = $this->userStoreIds();
|
||||
if (!in_array($account->store_id, $storeIds)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
}
|
||||
|
||||
$newParentId = $request->input('parent_id');
|
||||
$newParentId = ($newParentId === null || $newParentId === '' || $newParentId == 0) ? null : (int)$newParentId;
|
||||
|
||||
if ($newParentId === (int)$account->id) {
|
||||
return response()->json(['success' => false, 'message' => 'An account cannot be its own parent.'], 422);
|
||||
}
|
||||
|
||||
if ($newParentId !== null) {
|
||||
$parent = Account::find($newParentId);
|
||||
if (!$parent) {
|
||||
return response()->json(['success' => false, 'message' => 'Target parent not found.'], 422);
|
||||
}
|
||||
|
||||
$cursor = $parent;
|
||||
$hops = 0;
|
||||
while ($cursor && $hops < 1000) {
|
||||
if ((int)$cursor->id === (int)$account->id) {
|
||||
return response()->json(['success' => false, 'message' => 'Cannot move an account under one of its own descendants.'], 422);
|
||||
}
|
||||
$cursor = $cursor->parent_id ? Account::find($cursor->parent_id) : null;
|
||||
$hops++;
|
||||
}
|
||||
}
|
||||
|
||||
$account->parent_id = $newParentId;
|
||||
$account->updated_by = Auth::id();
|
||||
$account->save();
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-activate an archived account.
|
||||
*/
|
||||
public function restoreAccount(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$account = Account::find($request->input('id'));
|
||||
if (!$account) {
|
||||
return response()->json(['success' => false, 'message' => 'Account not found.'], 404);
|
||||
}
|
||||
|
||||
$type = $this->resolveType();
|
||||
$isBig3 = $this->isBig3($type);
|
||||
if (!$isBig3) {
|
||||
if (!$this->guardStore()) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
$storeIds = $this->userStoreIds();
|
||||
if (!in_array($account->store_id, $storeIds)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
}
|
||||
|
||||
$account->is_active = true;
|
||||
$account->updated_by = Auth::id();
|
||||
$account->save();
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme metadata + drift summary for Manage Accounts.
|
||||
*/
|
||||
public function getThemeInfo(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$current = \App\Support\AccountingTheme::current();
|
||||
$definition = \App\Support\AccountingTheme::definition($current);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'current' => $current,
|
||||
'definition' => [
|
||||
'label' => $definition['label'] ?? $current,
|
||||
'description' => $definition['description'] ?? null,
|
||||
'version' => $definition['version'] ?? 1,
|
||||
],
|
||||
'options' => \App\Support\AccountingTheme::options(),
|
||||
'counts' => [
|
||||
'theme_tagged' => Account::where('theme_key', $current)->count(),
|
||||
'user_added' => Account::whereNull('theme_key')->count(),
|
||||
'archived' => Account::where('is_active', false)->count(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only drift report.
|
||||
*/
|
||||
public function getThemeDrift(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => \App\Support\AccountingTheme::drift(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the active accounting theme.
|
||||
*/
|
||||
public function setTheme(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$key = trim((string) $request->input('key', ''));
|
||||
if ($key === '' || !\array_key_exists($key, \App\Support\AccountingTheme::all())) {
|
||||
return response()->json(['success' => false, 'message' => 'Unknown theme.'], 422);
|
||||
}
|
||||
|
||||
\App\Models\SystemSetting::setValue('accounting_theme', $key, 'accounting', 'string');
|
||||
|
||||
return response()->json(['success' => true, 'key' => $key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Idempotently re-apply the active theme (additive only).
|
||||
*/
|
||||
public function applyTheme(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
try {
|
||||
$stats = \App\Support\AccountingTheme::apply();
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to apply theme: ' . $e->getMessage(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => sprintf(
|
||||
'Theme "%s" applied: %d created, %d updated, %d unchanged.',
|
||||
$stats['theme_key'],
|
||||
$stats['created'],
|
||||
$stats['stamped'],
|
||||
$stats['skipped']
|
||||
),
|
||||
'data' => $stats,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recent transactions for the Reports page.
|
||||
*/
|
||||
public function listReports(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$type = $this->resolveType();
|
||||
$isBig3 = $this->isBig3($type);
|
||||
|
||||
if (!$isBig3 && !$this->guardStore()) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$storeIds = $isBig3 ? [] : $this->userStoreIds();
|
||||
$accountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id');
|
||||
|
||||
$transactions = AccountTransaction::with(['account', 'creator'])
|
||||
->whereIn('account_id', $accountIds)
|
||||
->orderBy('transaction_date', 'desc')
|
||||
->limit(100)
|
||||
->get();
|
||||
|
||||
return response()->json(['success' => true, 'transactions' => $transactions]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user