Added Wizard for Month/Year End Close

This commit is contained in:
2026-06-09 18:48:52 -05:00
parent 9a9f890494
commit 599465bdac
15 changed files with 1374 additions and 25 deletions

View File

@@ -171,6 +171,10 @@
v-if="activeView === 'gl-journals'"
:token="token"
/>
<CloseView
v-if="activeView === 'close'"
:token="token"
/>
<TemplatesView
v-if="activeView === 'templates'"
:category-items="categoryItems"
@@ -313,6 +317,7 @@ import AlertsView from './views/AlertsView.vue';
import AssignmentsView from './views/AssignmentsView.vue';
import BooksView from './views/BooksView.vue';
import GlJournalsView from './views/GlJournalsView.vue';
import CloseView from './views/CloseView.vue';
import ContactsView from './views/ContactsView.vue';
import AssetsView from './views/AssetsView.vue';
import DashboardView from './views/DashboardView.vue';
@@ -351,6 +356,7 @@ export default {
AssetsView,
BooksView,
GlJournalsView,
CloseView,
ChangePasswordCard,
ContactsView,
DashboardView,
@@ -534,6 +540,7 @@ export default {
reports: 'Depreciation schedules, monthly expense, and book values',
books: 'Configure books, assign tax rule sets, and set entry defaults',
'gl-journals': 'General ledger rollup and editable journal entries per book',
close: 'Guided month-end / year-end close: post, reconcile, and lock the period',
templates: 'JSON-driven entry defaults and custom fields',
'tax-rules': 'Upload, edit, export, and activate depreciation rule sets',
admin: 'Users, configuration, and integrations',
@@ -558,6 +565,7 @@ export default {
reports: 'Reports',
books: 'Books',
'gl-journals': 'GL and Journals',
close: 'Period Close',
templates: 'Templates',
'tax-rules': 'Tax rules',
admin: 'Configuration',
@@ -625,6 +633,7 @@ export default {
reports: ['finance.view'],
books: ['finance.manage'],
'gl-journals': ['finance.view', 'finance.manage'],
close: ['finance.close', 'finance.manage'],
'tax-rules': ['finance.manage'],
'admin-users': ['admin.users'],
'admin-security': ['admin.security'],

View File

@@ -0,0 +1,39 @@
<template>
<div style="overflow-x:auto">
<table class="asset-table">
<thead>
<tr><th v-for="col in columns" :key="col.key">{{ col.label }}</th></tr>
</thead>
<tbody>
<tr v-for="(row, i) in rows" :key="i">
<td v-for="col in columns" :key="col.key" :class="{ 'text-right': col.format === 'currency' }">
<slot :name="`cell-${col.key}`" :row="row">{{ display(row[col.key], col.format) }}</slot>
</td>
</tr>
<tr v-if="!rows.length && empty">
<td :colspan="columns.length" class="quiet">{{ empty }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import { currency } from '../utils/format';
// Compact, read-only table for the close wizard's step panels. Columns are { key, label, format }; a
// `cell-<key>` slot overrides rendering for a column. Lighter than DataTable (no search/pagination).
export default {
props: {
rows: { type: Array, default: () => [] },
columns: { type: Array, required: true },
empty: { type: String, default: '' }
},
methods: {
display(value, format) {
if (value === null || value === undefined || value === '') return format === 'currency' ? currency(0) : '';
return format === 'currency' ? currency(value) : value;
}
}
};
</script>

View File

@@ -374,6 +374,126 @@
</p>
</section>
<!-- PERIOD CLOSE -->
<section v-show="isShown('close')" class="manual-section">
<h2>Period close month-end &amp; year-end</h2>
<p>
<strong>Financial Period Close</strong> is a guided wizard that walks you through closing an accounting
period for the fixed-asset subledger. It reviews the period's activity, <strong>posts the journal entries the
close generates into the general ledger</strong>, reconciles the Fixed Asset Register to the GL, and then
<strong>locks the period</strong> so it can't be changed after the fact. Every action is recorded in
<a href="#" @click.prevent="select('admin')">Activity &amp; Audit Trail</a>.
</p>
<h3>Scope: which book, which period</h3>
<ul>
<li>A close runs against <strong>one book</strong> pick it at the top of the screen; it defaults to your
<strong>primary book</strong> (the one that feeds the financial GL). Run a separate close for each book you
maintain (for example, close GAAP for the financial GL; tax books are reviewed at year-end).</li>
<li>Choose <strong>Month</strong> (a fiscal year + month) or <strong>Year</strong> (a full fiscal year), then
<strong>Start / Resume</strong>. A close in progress is saved, so you can leave and come back to it. The
<strong>Recent closes</strong> list shows every period with its status and a 🔒 lock badge.</li>
</ul>
<h3>The month-end checklist</h3>
<p>Each step shows the live data for the period and lets you act, then mark it done:</p>
<ol>
<li><strong>Record additions</strong> review capital purchases placed in service in the period. Any asset
below your <strong>capitalization threshold</strong> is flagged so you can confirm it belongs on the register.</li>
<li><strong>Record disposals</strong> review assets sold, scrapped, or retired in the period (cost,
accumulated depreciation, gain/loss). You can <strong>post the disposal journal entries</strong> here.</li>
<li><strong>Capitalize WIP</strong> review Construction-in-progress assets and <strong>capitalize</strong>
completed ones, which moves them into service and begins depreciation.</li>
<li><strong>Process &amp; post depreciation</strong> compute the period's depreciation and <strong>post it to
the GL</strong>, allocated by department / cost center. Fully-depreciated assets that would still compute
depreciation are flagged for review.</li>
<li><strong>Check for impairments</strong> — review flagged assets and record any impairment loss (from the
asset's Adjustments tab).</li>
<li><strong>Reconcile subledger to GL</strong> compare the register to the GL control accounts and clear or
acknowledge any variance (see below). <em>Required to finalize.</em></li>
<li><strong>Generate reports</strong> run and file the Fixed Asset Register, Depreciation Summary, and
Additions / Disposals reports for your records.</li>
<li><strong>Finalize &amp; lock</strong> freeze the period.</li>
</ol>
<h3>What gets posted to the GL</h3>
<p>
The close writes <strong>real, balanced journal entries</strong> into the stored GL layer (visible under
<a href="#" @click.prevent="select('books')">Books GL and Journals</a>). The accounts come from your
<strong>GL posting defaults</strong> (per-asset overrides win where set):
</p>
<table class="manual-table">
<thead><tr><th>Event</th><th>Debit</th><th>Credit</th></tr></thead>
<tbody>
<tr><td>Period depreciation (by department)</td><td>Depreciation Expense</td><td>Accumulated Depreciation</td></tr>
<tr><td>Capitalize WIP</td><td>Fixed Asset cost</td><td>Construction-in-progress (CWIP)</td></tr>
<tr><td>Disposal</td><td>Accum. depreciation + Proceeds (+ Loss)</td><td>Asset cost (+ Gain)</td></tr>
<tr><td>Year-end roll-forward</td><td>Retained Earnings</td><td>Depreciation Expense</td></tr>
</tbody>
</table>
<v-alert type="info" variant="tonal" density="comfortable" class="manual-callout">
Every close posting is <strong>tagged to its period</strong>, which makes posting <strong>idempotent</strong>:
re-running a step (for example, re-posting depreciation after a correction) replaces the prior entries rather
than duplicating them. Manage the accounts these entries use in
<a href="#" @click.prevent="select('books')">Books GL and Journals Chart of accounts</a> (the
<strong>posting defaults</strong>: asset cost, accumulated depreciation, depreciation expense, CWIP, gain/loss,
proceeds clearing, and retained earnings).
</v-alert>
<h3>Reconciling the subledger to the GL</h3>
<p>
The <strong>Reconcile</strong> step compares the Fixed Asset Register (the subledger) to the GL
control-account balances <strong>cost</strong>, <strong>accumulated depreciation</strong>, and
<strong>net book value</strong> and shows the variance per account. A green <em>Balanced</em> badge means
they agree; otherwise the variance is shown so you can clear it (post an adjusting entry) or
<strong>acknowledge</strong> it and finalize anyway.
</p>
<v-alert type="info" variant="tonal" density="comfortable" class="manual-callout">
When you <em>first</em> adopt closes, the GL won't yet hold the asset's full history, so the register
(life-to-date) and the GL (only what's been posted) will differ — that opening variance is expected. Post an
opening-balance entry to clear it, or acknowledge it. Once you run closes forward period over period, the two
stay in step.
</v-alert>
<h3>Finalizing &amp; the period lock</h3>
<p>
<strong>Finalize &amp; lock</strong> becomes available once the required steps are done and the reconciliation
has been run. Finalizing snapshots the period's totals and sets the period to <strong>Closed</strong>. A
closed period is <strong>locked</strong>:
</p>
<ul>
<li>Any change whose effective date falls inside the closed period is <strong>rejected</strong> including
<strong>GL journal entries</strong> (for that book), and <strong>disposals, impairments, and asset
financial-date / cost edits</strong> (gated by the <strong>primary</strong> book's locked periods, since
that book is the subledger that feeds the GL).</li>
<li>Edits to non-financial fields (location, custodian, notes, and so on) are <strong>not</strong> blocked.</li>
</ul>
<h3>Year-end close</h3>
<p>The year-end checklist includes everything above, plus:</p>
<ul>
<li><strong>Final month-end close</strong> — confirm the final month is closed and its depreciation posted.</li>
<li><strong>Physical inventory</strong> — confirm you've verified high-value / mobile assets still exist.</li>
<li><strong>Update tax books</strong> recompute tax depreciation (MACRS / §179) and review book-vs-tax
differences.</li>
<li><strong>Roll-forward</strong> finalizing a year close automatically posts the <strong>roll-forward</strong>
that clears the year's depreciation expense into <strong>Retained Earnings</strong> and locks the year.</li>
</ul>
<h3>Reopening a closed period</h3>
<p>
If you must correct a closed period, use <strong>Reopen</strong> (requires the close permission). Reopening
<strong>reverses all of that close's posted entries</strong>, returns the period to <em>in&nbsp;progress</em>,
and unlocks it so corrections can be made then you re-run and finalize again. The reopen, the reversed
entries, and the re-close are all audited.
</p>
<v-alert type="warning" variant="tonal" density="comfortable" class="manual-callout">
Running the close needs the <strong>Run month-end / year-end close &amp; reopen periods</strong> capability
(included in the Finance role). Because closing posts to the GL and locks the books, keep this permission to
the people responsible for the close.
</v-alert>
</section>
<!-- TAX RULES -->
<section v-show="isShown('taxrules')" class="manual-section">
<h2>Tax rule sets</h2>
@@ -1067,6 +1187,7 @@ export default {
{ id: 'lifecycle', title: 'Asset lifecycle', icon: 'mdi-cash-remove', keywords: 'dispose disposal sale retire gain loss lease amortization impairment adjustment write down' },
{ id: 'barcodes', title: 'Barcodes & scanning', icon: 'mdi-barcode-scan', keywords: 'barcode label print scan camera tag' },
{ id: 'books', title: 'Books, depreciation & tax', icon: 'mdi-book-open-variant', keywords: 'book gaap federal depreciation general ledger gl method convention 179 bonus reports journal entry entries import export csv excel conflict merge history zone section 179' },
{ id: 'close', title: 'Period close (month & year-end)', icon: 'mdi-calendar-lock', keywords: 'close month end year end period lock reconcile subledger gl roll forward retained earnings depreciation post reopen finalize wip capitalize disposal impairment audit' },
{ id: 'taxrules', title: 'Tax rule sets', icon: 'mdi-scale-balance', keywords: 'tax rules jurisdiction method limits activate version' },
{ id: 'templates', title: 'Templates', icon: 'mdi-form-select', keywords: 'template defaults custom fields data entry' },
{ id: 'pm', title: 'Preventative maintenance', icon: 'mdi-wrench-clock', keywords: 'pm maintenance plan steps complete close signature ratings guidance photos schedule meter usage reading hours lifespan wear curve dynamic nightly recompute' },

View File

@@ -161,6 +161,7 @@ export const navItems = [
children: [
{ key: 'books', label: 'Books', icon: 'mdi-book-open-variant' },
{ key: 'gl-journals', label: 'GL and Journals', icon: 'mdi-book-account-outline' },
{ key: 'close', label: 'Period Close', icon: 'mdi-calendar-lock' },
{ key: 'tax-rules', label: 'Tax rules', icon: 'mdi-scale-balance' }
]
},

383
src/views/CloseView.vue Normal file
View File

@@ -0,0 +1,383 @@
<template>
<section class="content-grid">
<!-- Launcher -->
<v-card class="span-12 panel-pad">
<div class="toolbar-row mb-1">
<h2 class="section-title">Period Close</h2>
</div>
<p class="quiet text-body-2 mb-3">
Guided month-end / year-end close for the fixed-asset subledger. Posts the period's journal entries to the GL,
reconciles the register, and locks the period against backdated changes. Every action is recorded in
Admin → Activity &amp; Audit Trail.
</p>
<div class="toolbar-row" style="flex-wrap:wrap;gap:12px">
<v-select v-model="form.book" :items="bookItems" label="Book" density="compact" hide-details style="max-width:200px" />
<v-select v-model="form.period_type" :items="periodTypeItems" item-title="title" item-value="value" label="Period" density="compact" hide-details style="max-width:140px" />
<v-text-field v-model.number="form.fiscal_year" type="number" label="Fiscal year" density="compact" hide-details style="max-width:130px" />
<v-select v-if="form.period_type === 'month'" v-model="form.period_month" :items="monthItems" item-title="title" item-value="value" label="Month" density="compact" hide-details style="max-width:160px" />
<v-btn color="primary" prepend-icon="mdi-play" :loading="starting" @click="startClose">Start / Resume</v-btn>
</div>
<v-alert v-if="error" type="error" density="compact" class="mt-3">{{ error }}</v-alert>
</v-card>
<!-- Active close: the wizard -->
<v-card v-if="period" class="span-8 panel-pad">
<div class="toolbar-row mb-3">
<div>
<h2 class="section-title">
{{ window.label }} · {{ period.book_code }}
<v-chip size="small" :color="statusColor" variant="tonal" class="ml-2">{{ period.status }}</v-chip>
<v-icon v-if="period.locked" icon="mdi-lock" color="error" class="ml-1" />
</h2>
<div class="quiet text-body-2">{{ period.period_type === 'year' ? 'Year-end close' : 'Month-end close' }}</div>
</div>
<v-spacer />
<v-btn v-if="period.locked" color="warning" variant="tonal" size="small" prepend-icon="mdi-lock-open" :loading="busy" @click="reopen">Reopen</v-btn>
<v-btn v-else color="success" prepend-icon="mdi-lock-check" :loading="busy" :disabled="!canFinalize" @click="finalize">Finalize &amp; lock</v-btn>
</div>
<v-alert v-if="!period.locked && !canFinalize" type="info" density="compact" variant="tonal" class="mb-3">
Complete the required steps to finalize: {{ missingRequired.join(', ') || 'run the reconciliation' }}.
</v-alert>
<v-alert v-if="message" type="success" density="compact" class="mb-3">{{ message }}</v-alert>
<!-- Step cards -->
<div v-for="(step, index) in steps" :key="step.key" class="close-step" :class="{ done: isDone(step.key) }">
<div class="toolbar-row">
<div class="step-num">{{ index + 1 }}</div>
<div>
<div class="font-weight-medium">
{{ step.title }}
<v-chip v-if="step.required" size="x-small" color="primary" variant="tonal" class="ml-1">required</v-chip>
<v-icon v-if="isDone(step.key)" icon="mdi-check-circle" color="success" size="small" class="ml-1" />
</div>
<div class="quiet text-caption">{{ step.description }}</div>
</div>
<v-spacer />
<v-btn v-if="!period.locked" size="x-small" :variant="isDone(step.key) ? 'tonal' : 'outlined'" :color="isDone(step.key) ? 'success' : undefined" @click="toggleStep(step.key)">
{{ isDone(step.key) ? 'Done' : 'Mark done' }}
</v-btn>
</div>
<div class="step-body">
<!-- Additions -->
<template v-if="step.key === 'additions'">
<div class="quiet text-caption mb-1" v-if="data.threshold">Capitalization threshold: {{ currency(data.threshold) }}</div>
<CloseTable :rows="data.additions" :columns="addCols" empty="No additions in this period.">
<template #cell-below="{ row }"><v-chip v-if="row.below_threshold" size="x-small" color="warning" variant="tonal">below threshold</v-chip></template>
</CloseTable>
</template>
<!-- Disposals -->
<template v-else-if="step.key === 'disposals'">
<CloseTable :rows="data.disposals" :columns="dispCols" empty="No disposals in this period." />
<v-btn v-if="!period.locked && data.disposals.length" size="x-small" variant="tonal" class="mt-2" prepend-icon="mdi-bank-transfer" :loading="busy" @click="postDisposals">Post disposal entries</v-btn>
</template>
<!-- WIP -->
<template v-else-if="step.key === 'wip'">
<CloseTable :rows="data.wip" :columns="wipCols" empty="No work-in-progress assets.">
<template #cell-action="{ row }">
<v-btn v-if="!period.locked" size="x-small" color="primary" variant="tonal" :loading="busy" @click="capitalize(row)">Capitalize</v-btn>
</template>
</CloseTable>
</template>
<!-- Depreciation -->
<template v-else-if="step.key === 'depreciation'">
<div class="ledger-summary mb-2">
<div class="ledger-chip"><span class="quiet">Period depreciation</span><strong>{{ currency(data.depreciation.total) }}</strong></div>
</div>
<CloseTable :rows="data.depreciation.by_department" :columns="deptCols" empty="No depreciation this period." />
<v-alert v-if="data.depreciation.fully_depreciated_still_running.length" type="warning" density="compact" variant="tonal" class="mt-2">
{{ data.depreciation.fully_depreciated_still_running.length }} fully-depreciated asset(s) still computing depreciation — review.
</v-alert>
<v-btn v-if="!period.locked" size="small" color="primary" class="mt-2" prepend-icon="mdi-bank-plus" :loading="busy" @click="postDepreciation">Post depreciation</v-btn>
</template>
<!-- Impairments -->
<template v-else-if="step.key === 'impairments'">
<CloseTable :rows="data.impairments" :columns="impairCols" empty="No impairment candidates flagged." />
<div class="quiet text-caption mt-1">Record impairments from the asset's Adjustments tab; they post when this period is reconciled.</div>
</template>
<!-- Reconcile -->
<template v-else-if="step.key === 'reconcile'">
<v-btn v-if="!period.locked" size="small" variant="tonal" prepend-icon="mdi-scale-balance" :loading="busy" @click="runReconcile">Run reconciliation</v-btn>
<div v-if="reconciliation" class="mt-2">
<v-chip size="small" :color="reconciliation.balanced ? 'success' : 'warning'" variant="tonal" class="mb-2">
{{ reconciliation.balanced ? 'Balanced' : `Variance ${currency(reconciliation.max_variance)} — clear or acknowledge` }}
</v-chip>
<CloseTable :rows="reconciliation.lines" :columns="reconCols" empty="" />
</div>
</template>
<!-- Reports -->
<template v-else-if="step.key === 'reports'">
<div class="quiet text-body-2">Run and file the final reports for the period:</div>
<ul class="quiet text-body-2 mt-1">
<li>Fixed Asset Register · Depreciation Summary · Additions / Disposals</li>
</ul>
</template>
<!-- Year-only confirm steps -->
<template v-else-if="['final_month_close', 'physical_inventory', 'tax_books'].includes(step.key)">
<div class="quiet text-body-2">{{ step.description }} Mark done once verified.</div>
</template>
</div>
</div>
</v-card>
<!-- History -->
<v-card :class="period ? 'span-4 panel-pad' : 'span-12 panel-pad'">
<h3 class="section-title mb-2">Recent closes</h3>
<v-list density="compact">
<v-list-item
v-for="p in periods"
:key="p.id"
:active="period && p.id === period.id"
rounded="sm"
@click="openPeriod(p.id)"
>
<v-list-item-title>{{ labelFor(p) }} · {{ p.book_code }}</v-list-item-title>
<template #append>
<v-icon v-if="p.status === 'closed'" icon="mdi-lock" color="error" size="small" />
<v-chip v-else size="x-small" variant="tonal">{{ p.status }}</v-chip>
</template>
</v-list-item>
<v-list-item v-if="!periods.length" class="quiet text-body-2">No closes yet.</v-list-item>
</v-list>
</v-card>
</section>
</template>
<script>
import { apiRequest } from '../services/api';
import { currency } from '../utils/format';
import CloseTable from '../components/CloseTable.vue';
// Period Close wizard (Financial → Period Close). Walks the month/year close checklist, posts the
// period's depreciation / WIP / disposal / roll-forward journal entries to the GL, reconciles the
// register to the GL, and finalizes (locks) the period. Server enforces gating, scoping, and the lock.
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
export default {
components: { CloseTable },
props: { token: { type: String, required: true } },
data() {
return {
books: [],
form: { book: 'GAAP', period_type: 'month', fiscal_year: new Date().getFullYear(), period_month: new Date().getMonth() + 1 },
periodTypeItems: [{ title: 'Month', value: 'month' }, { title: 'Year', value: 'year' }],
monthItems: MONTHS.map((title, i) => ({ title, value: i + 1 })),
period: null,
steps: [],
data: null,
periods: [],
starting: false,
busy: false,
error: '',
message: '',
addCols: [
{ key: 'asset_id', label: 'Asset' }, { key: 'description', label: 'Description' },
{ key: 'acquisition_cost', label: 'Cost', format: 'currency' }, { key: 'in_service_date', label: 'In service' }, { key: 'below', label: '' }
],
dispCols: [
{ key: 'asset_id', label: 'Asset' }, { key: 'description', label: 'Description' }, { key: 'disposal_date', label: 'Date' },
{ key: 'disposed_cost', label: 'Cost', format: 'currency' }, { key: 'accumulated_depreciation', label: 'Accum.', format: 'currency' }, { key: 'gain_loss', label: 'Gain/(loss)', format: 'currency' }
],
wipCols: [
{ key: 'asset_id', label: 'Asset' }, { key: 'description', label: 'Description' }, { key: 'acquisition_cost', label: 'Cost', format: 'currency' }, { key: 'action', label: '' }
],
deptCols: [{ key: 'department', label: 'Department / cost center' }, { key: 'amount', label: 'Depreciation', format: 'currency' }],
impairCols: [{ key: 'asset_id', label: 'Asset' }, { key: 'description', label: 'Description' }, { key: 'condition', label: 'Condition' }, { key: 'acquisition_cost', label: 'Cost', format: 'currency' }],
reconCols: [
{ key: 'account', label: 'Account' }, { key: 'subledger', label: 'Subledger', format: 'currency' },
{ key: 'gl', label: 'General ledger', format: 'currency' }, { key: 'variance', label: 'Variance', format: 'currency' }
]
};
},
computed: {
bookItems() {
return this.books.map((b) => b.code);
},
window() {
return this.data?.window || { label: '' };
},
reconciliation() {
return this.data?.reconciliation || this.period?.reconciliation || null;
},
statusColor() {
return { open: 'grey', in_progress: 'info', closed: 'success' }[this.period?.status] || undefined;
},
requiredKeys() {
return this.steps.filter((s) => s.required).map((s) => s.key);
},
missingRequired() {
return this.requiredKeys.filter((key) => !this.isDone(key));
},
canFinalize() {
return this.period && !this.period.locked && this.missingRequired.length === 0 && Boolean(this.reconciliation);
}
},
mounted() {
this.loadBooks();
this.loadPeriods();
},
methods: {
currency,
async api(path, options = {}) {
return apiRequest(path, this.token, options);
},
labelFor(p) {
return p.period_type === 'year' ? `FY ${p.fiscal_year}` : `${MONTHS[(p.period_month || 1) - 1].slice(0, 3)} ${p.fiscal_year}`;
},
isDone(key) {
return Boolean(this.period?.steps?.[key]?.done);
},
async loadBooks() {
try {
const data = await (await this.api('/api/books')).json();
this.books = data.books;
const primary = this.books.find((b) => b.is_primary);
if (primary) this.form.book = primary.code;
} catch (error) {
this.error = error.message;
}
},
async loadPeriods() {
try {
this.periods = (await (await this.api('/api/close-periods')).json()).periods;
} catch (error) {
this.error = error.message;
}
},
async startClose() {
this.starting = true;
this.error = '';
this.message = '';
try {
const data = await (await this.api('/api/close-periods/start', { method: 'POST', body: JSON.stringify(this.form) })).json();
await this.openPeriod(data.period.id);
await this.loadPeriods();
} catch (error) {
this.error = error.message;
} finally {
this.starting = false;
}
},
async openPeriod(id) {
this.error = '';
this.message = '';
try {
const data = await (await this.api(`/api/close-periods/${id}`)).json();
this.period = data.period;
this.steps = data.steps;
this.data = data.data;
} catch (error) {
this.error = error.message;
}
},
async refresh() {
if (this.period) await this.openPeriod(this.period.id);
},
async act(path, body) {
this.busy = true;
this.error = '';
this.message = '';
try {
const res = await (await this.api(path, { method: 'POST', body: JSON.stringify(body || {}) })).json();
await this.refresh();
return res;
} catch (error) {
this.error = error.message;
return null;
} finally {
this.busy = false;
}
},
async toggleStep(key) {
await this.act(`/api/close-periods/${this.period.id}/steps/${key}`, { done: !this.isDone(key) });
},
async postDepreciation() {
const res = await this.act(`/api/close-periods/${this.period.id}/post-depreciation`);
if (res) this.message = `Posted ${currency(res.summary.debit)} of depreciation across ${res.summary.expense_lines} line(s).`;
},
async postDisposals() {
const res = await this.act(`/api/close-periods/${this.period.id}/post-disposals`);
if (res) this.message = `Posted entries for ${res.result.posted} disposal(s).`;
},
async runReconcile() {
const res = await this.act(`/api/close-periods/${this.period.id}/reconcile`);
if (res) this.message = res.reconciliation.balanced ? 'Subledger reconciles to the GL.' : `Variance of ${currency(res.reconciliation.max_variance)} — clear it or mark the step done to acknowledge.`;
},
async capitalize(row) {
const date = window.prompt('In-service date (YYYY-MM-DD):', new Date().toISOString().slice(0, 10));
if (!date) return;
const res = await this.act(`/api/close-periods/${this.period.id}/wip/${row.id}/capitalize`, { in_service_date: date });
if (res) this.message = `Capitalized ${row.asset_id} into service.`;
},
async finalize() {
if (!window.confirm('Finalize and lock this period? Backdated changes will be blocked until it is reopened.')) return;
const res = await this.act(`/api/close-periods/${this.period.id}/finalize`, { acknowledge_variance: true });
if (res) {
this.message = 'Period closed and locked.';
await this.loadPeriods();
}
},
async reopen() {
if (!window.confirm('Reopen this closed period? Its posted close entries will be reversed.')) return;
const res = await this.act(`/api/close-periods/${this.period.id}/reopen`);
if (res) {
this.message = 'Period reopened; close entries reversed.';
await this.loadPeriods();
}
}
}
};
</script>
<style scoped>
.close-step {
border: 1px solid rgba(128, 128, 128, 0.2);
border-radius: 8px;
padding: 12px 14px;
margin-bottom: 12px;
}
.close-step.done {
border-color: rgba(76, 175, 80, 0.5);
}
.step-num {
width: 26px;
height: 26px;
border-radius: 50%;
background: rgba(var(--v-theme-primary), 0.12);
color: rgb(var(--v-theme-primary));
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.85rem;
margin-right: 6px;
}
.step-body {
margin-top: 10px;
margin-left: 32px;
}
.ledger-summary {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.ledger-chip {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 160px;
padding: 8px 12px;
border: 1px solid rgba(128, 128, 128, 0.25);
border-radius: 8px;
}
.ledger-chip strong {
font-size: 1.05rem;
}
</style>