initial: bootstrap from BukidBountyApp base

This commit is contained in:
Jonathan Sykes
2026-06-06 18:43:00 +08:00
commit eb4a5731fb
5674 changed files with 160857 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
---
task: Fix accounting theme switch+apply — switching theme should auto-seed accounts, apply should not silently fail
cycles: 5
context: true
private: false
started: 2026-05-29T00:00:00Z
finished: 2026-05-29T00:00:30Z
---
## files
- `resources/js/Pages/ManageAccounts.vue` [lines 457-491] — switchTheme() and reapplyTheme() JS functions
- `app/Http/Controllers/Accounting/AccountingController.php` [lines 677-715] — setTheme() and applyTheme() controllers; also getAccountsTree() lines 78-109 has a scoping bug
- `app/Support/AccountingTheme.php` [lines 201-276] — static apply() / applyNode() logic
## steps
### Bug 1 — "Switch" does not seed accounts (primary UX failure)
1. In `ManageAccounts.vue` `switchTheme()` (line ~457), after `POST /admin/accounting/theme/set` succeeds, immediately call `POST /admin/accounting/theme/apply` (reuse `reapplyTheme()` logic inline or call it).
Concrete change: after `await loadThemes(); await fetchAll();` inside the `if (res.data?.success)` block, also call `await reapplyTheme()` — OR refactor: make `switchTheme()` call a combined backend endpoint.
Simplest safe fix: after confirming `setTheme` success, call `reapplyTheme()` directly:
```js
if (res.data?.success) {
await setThemeKey(selectThemeKey.value); // already done
await reapplyTheme(); // NEW: seed accounts for the newly-active theme
}
```
Since `reapplyTheme()` reads the current theme from the backend (not a parameter), and `setTheme` already saved it, the order is correct.
### Bug 2 — `getAccountsTree()` discards scope on grandchildren (silent data loss)
2. In `AccountingController.php` `getAccountsTree()` (lines 100-102), the innermost `with()` closure does:
```php
$this->scopeAccounts($q2, $storeIds, $isBig3); // return value DISCARDED
```
Fix: capture the return value:
```php
$q2 = $this->scopeAccounts($q2, $storeIds, $isBig3);
```
Line 102 — change `$this->scopeAccounts($q2, $storeIds, $isBig3);` → `$q2 = $this->scopeAccounts($q2, $storeIds, $isBig3);`
### Bug 3 — `applyTheme()` has no error handling; DB failures are silent 500s
3. In `AccountingController.php` `applyTheme()` (line ~696), wrap the `AccountingTheme::apply()` call in try/catch:
```php
try {
$stats = \App\Support\AccountingTheme::apply();
} catch (\Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Failed to apply theme: ' . $e->getMessage(),
], 422);
}
```
### Bug 4 — switchTheme UX: don't allow re-clicking Switch on already-active theme after apply
4. After `reapplyTheme()` completes inside `switchTheme()`, call `loadThemes()` once more so `themeInfo.current` is refreshed and the Switch button re-disables correctly. (If `reapplyTheme()` already calls `loadThemes()`, this is automatic — just verify no double-call confusion.)
## context
### ManageAccounts.vue switchTheme() (lines 457-473):
```js
async function switchTheme() {
if (!selectThemeKey.value || selectThemeKey.value === themeInfo.value?.current) return;
switchingTheme.value = true;
try {
const res = await axios.post('/admin/accounting/theme/set', { key: selectThemeKey.value });
if (res.data?.success) {
await loadThemes();
await fetchAll();
// BUG: no apply call here — accounts never seeded
} else {
showNotice(res.data?.message || 'Could not switch theme.', { variant: 'danger' });
}
} catch (e) {
showNotice(e.response?.data?.message || 'Could not switch theme.', { variant: 'danger' });
} finally {
switchingTheme.value = false;
}
}
```
### ManageAccounts.vue reapplyTheme() (lines 475-491):
```js
async function reapplyTheme() {
applyingTheme.value = true;
try {
const res = await axios.post('/admin/accounting/theme/apply', {});
if (res.data?.success) {
showNotice(res.data.message || 'Theme applied.', { variant: 'success', title: 'Theme Applied' });
await loadThemes();
await fetchAll();
} else {
showNotice(res.data?.message || 'Apply failed.', { variant: 'danger' });
}
} catch (e) {
showNotice(e.response?.data?.message || 'Apply failed.', { variant: 'danger' });
} finally {
applyingTheme.value = false;
}
}
```
### AccountingController.php getAccountsTree() children scope (lines 96-105):
```php
->with(['children' => function ($q) use ($storeIds, $isBig3) {
$q = $q->where('is_active', true);
$q = $this->scopeAccounts($q, $storeIds, $isBig3); // ✓ captured
$q->with(['children' => function ($q2) use ($storeIds, $isBig3) {
$q2 = $q2->where('is_active', true);
$this->scopeAccounts($q2, $storeIds, $isBig3); // ✗ BUG: return discarded
}]);
}])
```
### AccountingController.php applyTheme() (lines 696-715):
```php
public function applyTheme(Request $request)
{
if (!UserPermissions::isActionPermitted(...)) {
return ResponseHelper::returnUnauthorized();
}
$stats = \App\Support\AccountingTheme::apply(); // no try-catch
return response()->json(['success' => true, 'message' => ..., 'data' => $stats]);
}
```
### AccountingTheme::apply() tree source:
- Reads `Config::get('accounting.themes', [])` from `config/accounting/themes.php`
- `banana_trading` key has `tree` with 4 root nodes (Sales, Supplier Purchases, Delivery & Logistics, Operating Expenses) + leaf children
- `blank` key has `'tree' => []` — if `current()` falls back to `blank`, apply creates 0 accounts
### Scope for Big3 vs store-level:
- `scopeAccounts($query, $storeIds, $isBig3)`: Big3 → `whereNull('store_id')`, store-level → `whereIn('store_id', $storeIds)`
- `applyNode()` creates accounts with `store_id` absent (NULL) → correct for Big3 global chart
- Root query: `whereNull('parent_id') AND is_active = 1 AND store_id IS NULL` → should match theme-applied accounts
## notes
- dictionary: ai-docs/dictionary.md
- linters: none detected
- constraints: Do NOT delete existing accounts; apply is idempotent/additive. Do NOT change the `setTheme` backend — it is correct as-is (just saves key). The frontend fix in `switchTheme()` is the primary fix. Wrap `reapplyTheme()` call inside `switchTheme()` with `switchingTheme.value = true` still active so the button stays disabled during the full switch+apply operation.