752 lines
27 KiB
PHP
752 lines
27 KiB
PHP
<?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]);
|
|
}
|
|
}
|