Updated GL System , Updated user manual
This commit is contained in:
@@ -266,8 +266,34 @@
|
||||
<p>
|
||||
Select a book to view its <strong>GL ledger</strong> in debit/credit format — asset cost, accumulated depreciation, and the
|
||||
period’s depreciation expense rolled to GL accounts, with per-asset detail and a summary. You can export the ledger.
|
||||
This rollup is computed automatically from depreciation; the <strong>Journal entries</strong> panel below it is a separate,
|
||||
editable layer (the two do not double-count).
|
||||
</p>
|
||||
|
||||
<h3>GL journal entries — add, edit, import & track changes</h3>
|
||||
<p>
|
||||
Below the computed ledger, the <strong>Journal entries</strong> panel holds stored, editable GL postings for the selected
|
||||
book and year. Use it to record manual journal lines or to load entries from another system.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Add / edit / delete.</strong> Click <strong>Add entry</strong> (or the pencil/trash on a row) to capture a
|
||||
date, GL account, account type, description, reference, <strong>debit</strong> and/or <strong>credit</strong>, and an
|
||||
optional <strong>external ID</strong>. The panel shows running debit, credit, and balance totals.</li>
|
||||
<li><strong>Import (CSV or Excel/XLS).</strong> Click <strong>Import</strong> and choose a <code>.csv</code>,
|
||||
<code>.xlsx</code>, or <code>.xls</code> file. Column headers are matched flexibly (Date, Account, Debit, Credit,
|
||||
Reference, Description, External Id, …).</li>
|
||||
<li><strong>Conflict / merge resolution.</strong> The import first shows a <strong>preview</strong> that classifies every
|
||||
row as <em>New</em>, <em>Update</em> (matches an existing entry but differs), <em>Duplicate</em> (identical), or
|
||||
<em>Error</em>, with counts. Rows are matched to existing entries by <strong>external ID</strong> first, then by a
|
||||
natural key (date + account + debit + credit + reference). You then pick a strategy — <strong>Skip conflicts</strong>,
|
||||
<strong>Merge</strong> (update existing + insert new), or <strong>Insert as new</strong> — and confirm. Set an external ID
|
||||
on your entries for the most reliable re-import matching.</li>
|
||||
<li><strong>Export.</strong> The <strong>Export</strong> menu downloads the current entries as CSV or Excel (XLSX).</li>
|
||||
<li><strong>Change history.</strong> Every add, edit, delete, and import is fully logged. Click the <strong>history</strong>
|
||||
(clock) icon on a row to see that entry’s timeline — who changed it, when, and a field-level before→after diff. The same
|
||||
events are searchable system-wide in <strong>Admin → Activity & Audit Trail</strong>.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Depreciation methods & critical fields</h3>
|
||||
<p>
|
||||
Each book’s depreciation is computed from the depreciation-critical fields: property type, placed-in-service date,
|
||||
@@ -523,6 +549,36 @@
|
||||
Completed forms — with signature, ratings, notes, and photos — are viewable from both the asset’s PM tab and the
|
||||
<strong>Completed PM forms</strong> list on the Maintenance screen.
|
||||
</v-alert>
|
||||
|
||||
<h3>Dynamic scheduling — usage meters & the lifespan wear curve</h3>
|
||||
<p>
|
||||
Beyond a fixed calendar interval, a schedule’s next-due date can adjust to real-world use. When usage and
|
||||
age signals are enabled, the next service is due at the <strong>earliest</strong> of three triggers — the
|
||||
calendar interval, a projected usage threshold, or an age-compressed date — and the asset’s PM list shows a
|
||||
small badge (📅 date, ⏱ usage, or 📉 age) indicating which one is driving the date.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Usage meters.</strong> Add one or more meters to a PM plan (e.g. <em>Operating Hours</em>, kWh,
|
||||
gallons, cycles) with a “due every” threshold. Record the current reading when completing a service, or
|
||||
anytime via <strong>Log reading</strong> on the asset’s PM tab. DepreCore infers the usage <em>rate</em>
|
||||
from successive readings and pulls the due-date in when a machine is running hard; crossing a threshold
|
||||
raises a usage alert (e.g. “Operating Hours 512/500”).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Lifespan wear curve.</strong> Turn on “Tighten the interval as the asset ages” on a plan to shorten
|
||||
the service interval as an asset approaches its estimated life (from the asset’s useful-life months). New
|
||||
equipment is serviced on the base cadence; near end-of-life it is serviced more often, along a curve you
|
||||
tune in PM settings.
|
||||
</li>
|
||||
</ul>
|
||||
<v-alert type="info" variant="tonal" density="comfortable" class="manual-callout">
|
||||
Both signals are <strong>opt-in</strong>: an administrator enables “Usage-based scheduling” and the “Lifespan
|
||||
wear curve” (with its end-of-life factor and aggressiveness) under <strong>Admin → Application Configuration →
|
||||
Preventative maintenance defaults</strong>. A nightly job re-projects due-dates so usage/age schedules stay
|
||||
current even when an asset isn’t serviced for a while; admins can also run it on demand with <strong>Run
|
||||
now</strong>.
|
||||
</v-alert>
|
||||
</section>
|
||||
|
||||
<!-- PARTS -->
|
||||
@@ -561,6 +617,7 @@
|
||||
<li>Use the type selector and filter box to find records; create a contact with <strong>New contact</strong>.</li>
|
||||
<li>Capture name/organization, international-friendly address and phone, and email.</li>
|
||||
<li><strong>Type-specific fields</strong>: employee ID/department/manager/start date; contractor tax ID or business license; vendor and service-provider star rating and credit term.</li>
|
||||
<li><strong>Location map</strong> — once an address is entered, the contact card can show an embedded <strong>Google map</strong> of the location, with an <em>Open in Google Maps</em> link. For a new contact the map appears on a <em>Show map</em> click; when you open an existing contact with an address it’s shown automatically.</li>
|
||||
<li>Keep a per-contact <strong>notes log</strong> for running history.</li>
|
||||
</ul>
|
||||
<h3>How contacts connect to the rest of the app</h3>
|
||||
@@ -605,6 +662,7 @@
|
||||
<ul>
|
||||
<li><strong>Email digest</strong> — when email is enabled and a mail server is configured, new alerts are emailed to the recipient list. You can also trigger <strong>Email digest now</strong> from the Alerts screen.</li>
|
||||
<li><strong>Webhook</strong> — each new alert can be posted to an external URL as a JSON object, optionally signed for authenticity. Useful for chat tools or custom automation.</li>
|
||||
<li><strong>Microsoft Teams</strong> — post a digest card of new alerts to a Teams channel or chat (see <a href="#" @click.prevent="select('teams')">Microsoft Teams integration</a>).</li>
|
||||
<li><strong>ServiceNow incident</strong> — raise a ticket from any alert (see below).</li>
|
||||
</ul>
|
||||
|
||||
@@ -655,21 +713,65 @@
|
||||
</v-alert>
|
||||
</section>
|
||||
|
||||
<!-- MICROSOFT TEAMS -->
|
||||
<section v-show="isShown('teams')" class="manual-section">
|
||||
<h2>Microsoft Teams integration</h2>
|
||||
<p>
|
||||
DepreCore can post newly raised alerts to a <strong>Microsoft Teams</strong> channel or chat. One digest card is sent per
|
||||
alert cycle, alongside (or instead of) email, webhook, and ServiceNow delivery. Configure it in
|
||||
<strong>Administration → Integrations → Microsoft Teams</strong>.
|
||||
</p>
|
||||
<h3>Connecting</h3>
|
||||
<ol>
|
||||
<li>In Teams, create an incoming webhook for the target channel — a <strong>Workflows</strong> “post to a channel when a
|
||||
webhook request is received” flow (recommended), or a legacy <strong>Incoming Webhook</strong> connector.</li>
|
||||
<li>Paste the webhook URL into DepreCore and pick the matching <strong>card format</strong>: <em>Adaptive Card (Workflows)</em>
|
||||
for the modern flow, or <em>MessageCard (legacy connector)</em>.</li>
|
||||
<li>Choose a <strong>minimum severity</strong> to post (e.g. Warning and above) so the channel only gets what matters, turn on
|
||||
<strong>Enable Teams delivery</strong>, and click <strong>Send test card</strong> to confirm it lands in the channel.</li>
|
||||
</ol>
|
||||
<v-alert type="info" variant="tonal" density="comfortable" class="manual-callout">
|
||||
Cards are colour-coded by severity and summarize the pending alerts (title, detail, due date, asset). Each alert is delivered
|
||||
once across all channels. <em>Note:</em> this “Teams” (Microsoft Teams chat) is unrelated to <strong>Admin → Users &
|
||||
Teams</strong>, which groups your DepreCore users.
|
||||
</v-alert>
|
||||
</section>
|
||||
|
||||
<!-- ADMIN -->
|
||||
<section v-show="isShown('admin')" class="manual-section">
|
||||
<h2>Administration</h2>
|
||||
<p>
|
||||
<strong>Admin</strong> (admin role) is where the application is configured. It is split into three sub-screens in the
|
||||
navigation rail: <strong>Users & Teams</strong>, <strong>Application Configuration</strong> (application settings, asset classes, depreciation zones, Section 179 limits, asset ID templates, asset categories,
|
||||
asset departments, PM categories, parts categories, maintenance defaults, tax rule sets), and <strong>Integrations</strong> (email, webhook, ServiceNow, Workday). Each is described below.
|
||||
<strong>Admin</strong> (admin role) is where the application is configured. It is split into sub-screens in the
|
||||
navigation rail: <strong>Users & Teams</strong>, <strong>Password & Security</strong>,
|
||||
<strong>Activity & Audit Trail</strong>, <strong>Application Configuration</strong> (companies, application
|
||||
settings, asset classes, depreciation zones, Section 179 limits, asset ID templates, asset categories,
|
||||
asset departments, PM categories, parts categories, maintenance defaults, tax rule sets), and
|
||||
<strong>Integrations</strong> (email, webhook, Microsoft Teams, ServiceNow, Workday). Each is described below.
|
||||
</p>
|
||||
|
||||
<h3>Users & roles</h3>
|
||||
<ul>
|
||||
<li>Create and edit users — name, email, team, role, status, and password.</li>
|
||||
<li>The list paginates and is filterable. A user’s role controls what they can see and do.</li>
|
||||
<li><strong>Company access</strong> — assign which companies a user may see and switch between (administrators
|
||||
always see every company). See <a href="#" @click.prevent="select('companies')">Companies</a>.</li>
|
||||
<li>You can require a <strong>password change at next sign-in</strong>, and a locked account (after too many
|
||||
failed sign-ins) shows an <strong>Unlock</strong> action — see
|
||||
<a href="#" @click.prevent="select('security')">Password & security</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Companies</h3>
|
||||
<p>Add, edit, and dispose/restore companies, each with its own books, GL, and data. Covered under
|
||||
<a href="#" @click.prevent="select('companies')">Companies</a>.</p>
|
||||
|
||||
<h3>Password & security</h3>
|
||||
<p>Password complexity/expiry/history rules and account lockout. Covered under
|
||||
<a href="#" @click.prevent="select('security')">Password & security</a>.</p>
|
||||
|
||||
<h3>Activity & audit trail</h3>
|
||||
<p>A searchable, exportable log of every change made in the system. Covered under
|
||||
<a href="#" @click.prevent="select('security')">Password & security</a>.</p>
|
||||
|
||||
<h3>Teams</h3>
|
||||
<p>Group users into teams. Select a team to edit or delete it (a team in use by members can’t be deleted until they’re reassigned).</p>
|
||||
|
||||
@@ -747,11 +849,20 @@
|
||||
payload is shown for whoever builds the receiving side.
|
||||
</p>
|
||||
|
||||
<h3>Microsoft Teams</h3>
|
||||
<p>Post alert digests to a Teams channel — webhook URL, card format, and minimum severity. Covered under <a href="#" @click.prevent="select('teams')">Microsoft Teams integration</a>.</p>
|
||||
|
||||
<h3>ServiceNow integration</h3>
|
||||
<p>Connection, incident push, and CMDB pull settings — covered in detail under <a href="#" @click.prevent="select('servicenow')">ServiceNow integration</a>.</p>
|
||||
|
||||
<h3>Maintenance (PM) defaults</h3>
|
||||
<p>Set the default maintenance frequency for newly attached plans and how many days ahead PM-due alerts should appear.</p>
|
||||
<p>
|
||||
Set the default maintenance frequency for newly attached plans and how many days ahead PM-due alerts should appear.
|
||||
This is also where you enable <strong>dynamic scheduling</strong>: <strong>usage-based scheduling</strong> (meters) and the
|
||||
<strong>lifespan wear curve</strong> (with its end-of-life interval factor and curve aggressiveness), plus the
|
||||
<strong>nightly recompute</strong> job (on/off, hour of day, and a <em>Run now</em> button). See
|
||||
<a href="#" @click.prevent="select('pm')">Preventative maintenance</a> for how these signals drive due-dates.
|
||||
</p>
|
||||
|
||||
<h3>Workday connector</h3>
|
||||
<ul>
|
||||
@@ -799,6 +910,83 @@
|
||||
</v-alert>
|
||||
</section>
|
||||
|
||||
<!-- COMPANIES -->
|
||||
<section v-show="isShown('companies')" class="manual-section">
|
||||
<h2>Companies (multi-company)</h2>
|
||||
<p>
|
||||
DepreCore can manage <strong>multiple companies</strong>, each with its own assets, books and general ledger, PM plans,
|
||||
alerts, reports, reference data (categories/locations/departments), contacts, templates, saved reports, parts, and
|
||||
employee directory. You work in one <strong>active company</strong> at a time.
|
||||
</p>
|
||||
<h3>Switching companies</h3>
|
||||
<p>
|
||||
When you have access to more than one company, a <strong>company switcher</strong> appears in the top bar. Choosing a
|
||||
company instantly re-scopes every screen — dashboard, assets, books, maintenance, alerts, reports, and so on — to that
|
||||
company. Your selection is remembered between visits.
|
||||
</p>
|
||||
<h3>Managing companies</h3>
|
||||
<p>Administrators manage companies in <strong>Admin → Application Configuration → Companies</strong>:</p>
|
||||
<ul>
|
||||
<li><strong>Add / edit</strong> a company — name, legal name, base currency, fiscal year-end month, and separate
|
||||
<strong>Federal</strong>, <strong>State</strong>, and (optional) <strong>Local</strong> tax IDs. A new company is
|
||||
created with the standard book set and the standard reference data so its ledger works immediately.</li>
|
||||
<li><strong>Dispose (archive)</strong> a company to retire it — all of its assets, books, GL, and history are
|
||||
<em>preserved</em>, but it’s hidden from the switcher and can’t be selected for new work. The last active company can’t be
|
||||
disposed. Disposed companies can be <strong>restored</strong> at any time.</li>
|
||||
</ul>
|
||||
<h3>Who can see which company</h3>
|
||||
<p>
|
||||
Each user is granted access to one or more companies (set in the user’s edit dialog — see
|
||||
<a href="#" @click.prevent="select('admin')">Administration</a>); the switcher only lists the companies they may use, and a
|
||||
user can’t reach another company’s data. <strong>Administrators see every company.</strong>
|
||||
</p>
|
||||
<v-alert type="info" variant="tonal" density="comfortable" class="manual-callout">
|
||||
A few things are deliberately <strong>shared across all companies</strong>: the tax-rule-set catalog (each company still
|
||||
selects which set each of its books uses), IRS reference data (depreciation zones and §179 limits), the integrations
|
||||
(email/Teams/webhook/ServiceNow/Workday), and the users/roles and audit/security layer.
|
||||
</v-alert>
|
||||
</section>
|
||||
|
||||
<!-- SECURITY -->
|
||||
<section v-show="isShown('security')" class="manual-section">
|
||||
<h2>Password & security · audit trail</h2>
|
||||
|
||||
<h3>Password & account security policy</h3>
|
||||
<p>
|
||||
Administrators set the rules for locally managed passwords in <strong>Admin → Password & Security</strong>. They are
|
||||
enforced whenever a password is set or changed and at sign-in:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Complexity</strong> — minimum length and required character classes (uppercase, lowercase, number, symbol),
|
||||
and a rule that a password may not contain the user’s name or email.</li>
|
||||
<li><strong>Rotation & reuse</strong> — expire passwords after N days, block reuse of the last N passwords, and an
|
||||
optional minimum password age.</li>
|
||||
<li><strong>Account lockout</strong> — lock an account for a set number of minutes after too many failed sign-ins. A locked
|
||||
user is refused until the window passes or an administrator clicks <strong>Unlock</strong> on the user.</li>
|
||||
</ul>
|
||||
<p>
|
||||
The live rule preview shows users exactly what’s required. Users change their own password from the account menu
|
||||
(<strong>Change password</strong>); when a change is required (expired, or set by an admin) they’re prompted at sign-in.
|
||||
</p>
|
||||
|
||||
<h3>Activity & Audit Trail</h3>
|
||||
<p>
|
||||
<strong>Admin → Activity & Audit Trail</strong> is a complete, searchable record of user activity — sign-ins and
|
||||
lockouts, password and permission changes, GL and data edits, imports, and every create/update/delete across the app.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Filter by <strong>user</strong>, <strong>action</strong>, <strong>entity type</strong>, <strong>date range</strong>, or
|
||||
free-text search, with pagination.</li>
|
||||
<li>Each entry can expand to show the full <strong>before → after</strong> detail of what changed.</li>
|
||||
<li><strong>Export CSV</strong> for external review or retention.</li>
|
||||
</ul>
|
||||
<v-alert type="info" variant="tonal" density="comfortable" class="manual-callout">
|
||||
Because every change is captured here with full before/after detail, the audit trail is the system-of-record for change
|
||||
tracking — for example, each GL journal-entry edit is logged here and also surfaced inline via the entry’s
|
||||
<strong>history</strong> icon (see <a href="#" @click.prevent="select('books')">Books, depreciation & tax</a>).
|
||||
</v-alert>
|
||||
</section>
|
||||
|
||||
<div class="manual-footer quiet">DepreCore User Guide · select a topic on the left, or use Print / Save PDF for the full manual.</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -878,16 +1066,19 @@ export default {
|
||||
{ id: 'assets', title: 'Assets — creating & managing', icon: 'mdi-archive-search-outline', keywords: 'asset register create edit import export mass edit workbench files notes tasks warranty' },
|
||||
{ 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' },
|
||||
{ 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: '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' },
|
||||
{ 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' },
|
||||
{ id: 'parts', title: 'Parts inventory', icon: 'mdi-package-variant-closed', keywords: 'parts stock aisle bin reserve reorder supplier inventory' },
|
||||
{ id: 'contacts', title: 'Contacts & CRM', icon: 'mdi-card-account-details-outline', keywords: 'contacts crm vendor employee contractor work location supplier notes' },
|
||||
{ id: 'assignments', title: 'Assignments (custody)', icon: 'mdi-account-switch-outline', keywords: 'assignment custody employee release traceability' },
|
||||
{ id: 'alerts', title: 'Alerts & reminders', icon: 'mdi-bell-alert-outline', keywords: 'alerts reminders severity scan acknowledge dismiss email digest webhook servicenow' },
|
||||
{ id: 'alerts', title: 'Alerts & reminders', icon: 'mdi-bell-alert-outline', keywords: 'alerts reminders severity scan acknowledge dismiss email digest webhook teams microsoft servicenow' },
|
||||
{ id: 'servicenow', title: 'ServiceNow integration', icon: 'mdi-cloud-sync-outline', keywords: 'servicenow incident ticket cmdb import sync connection' },
|
||||
{ id: 'admin', title: 'Administration', icon: 'mdi-cog-outline', keywords: 'admin users roles teams settings smtp email webhook workday configuration' },
|
||||
{ id: 'teams', title: 'Microsoft Teams', icon: 'mdi-microsoft-teams', keywords: 'teams microsoft chat channel webhook adaptive card messagecard alert digest notification workflow connector' },
|
||||
{ id: 'admin', title: 'Administration', icon: 'mdi-cog-outline', keywords: 'admin users roles teams settings smtp email webhook workday configuration company companies password security lockout audit activity trail' },
|
||||
{ id: 'companies', title: 'Companies (multi-company)', icon: 'mdi-domain', keywords: 'company companies multi tenant switch active dispose archive restore tax id federal state local entity access' },
|
||||
{ id: 'security', title: 'Password & security · audit', icon: 'mdi-shield-key-outline', keywords: 'password policy complexity expiry lockout reuse history audit trail activity log change tracking export forced change' },
|
||||
{ id: 'roles', title: 'Roles & permissions', icon: 'mdi-shield-account-outline', keywords: 'roles permissions admin finance operations viewer access' }
|
||||
]
|
||||
};
|
||||
|
||||
@@ -149,6 +149,159 @@
|
||||
<div v-else class="quiet text-body-2">Select a book to view its general ledger.</div>
|
||||
</v-card>
|
||||
|
||||
<!-- GL journal entries (stored, editable, fully audited) -->
|
||||
<v-card class="span-12 panel-pad">
|
||||
<div class="toolbar-row mb-3">
|
||||
<div>
|
||||
<h2 class="section-title">Journal entries</h2>
|
||||
<div class="quiet text-body-2">
|
||||
Manual & imported GL postings for <strong>{{ ledgerBook }}</strong> ({{ ledgerYear }}). All changes are
|
||||
recorded in Admin → Activity & Audit Trail.
|
||||
</div>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<v-btn size="small" variant="tonal" prepend-icon="mdi-upload" @click="$refs.glFile.click()">Import</v-btn>
|
||||
<input ref="glFile" hidden type="file" accept=".csv,.xlsx,.xls" @change="onImportFile" />
|
||||
<v-menu>
|
||||
<template #activator="{ props }">
|
||||
<v-btn v-bind="props" size="small" variant="tonal" prepend-icon="mdi-download">Export</v-btn>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item title="CSV" @click="exportEntries('csv')" />
|
||||
<v-list-item title="Excel (XLSX)" @click="exportEntries('xlsx')" />
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-btn size="small" color="primary" prepend-icon="mdi-plus" @click="openEntry()">Add entry</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="ledger-summary mb-3">
|
||||
<div class="ledger-chip"><span class="quiet">Entries</span><strong>{{ entrySummary.count }}</strong></div>
|
||||
<div class="ledger-chip"><span class="quiet">Debit</span><strong>{{ currency(entrySummary.debit) }}</strong></div>
|
||||
<div class="ledger-chip"><span class="quiet">Credit</span><strong>{{ currency(entrySummary.credit) }}</strong></div>
|
||||
<div class="ledger-chip"><span class="quiet">Balance</span><strong>{{ currency(entrySummary.balance) }}</strong></div>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
:columns="entryColumns"
|
||||
:rows="entries"
|
||||
row-key="id"
|
||||
:page-size="15"
|
||||
has-actions
|
||||
searchable
|
||||
search-placeholder="Filter entries"
|
||||
empty-text="No journal entries for this book and year."
|
||||
>
|
||||
<template #cell-debit="{ row }">{{ row.debit ? currency(row.debit) : '' }}</template>
|
||||
<template #cell-credit="{ row }">{{ row.credit ? currency(row.credit) : '' }}</template>
|
||||
<template #cell-source="{ row }"><v-chip size="x-small" variant="tonal">{{ row.source }}</v-chip></template>
|
||||
<template #actions="{ row }">
|
||||
<v-btn icon="mdi-history" size="small" variant="text" title="Change history" @click="openHistory(row)" />
|
||||
<v-btn icon="mdi-pencil" size="small" variant="text" @click="openEntry(row)" />
|
||||
<v-btn icon="mdi-delete-outline" size="small" variant="text" color="error" @click="removeEntry(row)" />
|
||||
</template>
|
||||
</DataTable>
|
||||
<v-alert v-if="entryError" type="error" density="compact" class="mt-2">{{ entryError }}</v-alert>
|
||||
<v-alert v-if="message" type="success" density="compact" class="mt-2">{{ message }}</v-alert>
|
||||
</v-card>
|
||||
|
||||
<!-- Add / edit GL entry -->
|
||||
<v-dialog v-model="entryDialog" max-width="640">
|
||||
<v-card>
|
||||
<v-card-title>{{ entryForm.id ? 'Edit GL entry' : 'New GL entry' }}</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="form-grid">
|
||||
<v-text-field v-model="entryForm.entry_date" type="date" label="Date *" />
|
||||
<v-text-field v-model="entryForm.account" label="GL account *" />
|
||||
<v-text-field v-model="entryForm.account_type" label="Account type" />
|
||||
<v-text-field v-model="entryForm.reference" label="Reference" />
|
||||
<v-text-field v-model.number="entryForm.debit" type="number" prefix="$" label="Debit" />
|
||||
<v-text-field v-model.number="entryForm.credit" type="number" prefix="$" label="Credit" />
|
||||
<v-text-field v-model="entryForm.external_id" class="full" label="External ID (used to match on re-import)" />
|
||||
<v-textarea v-model="entryForm.description" class="full" label="Description" rows="2" />
|
||||
</div>
|
||||
<v-alert v-if="entryDialogError" type="error" density="compact" class="mt-2">{{ entryDialogError }}</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="entryDialog = false">Cancel</v-btn>
|
||||
<v-btn color="primary" prepend-icon="mdi-content-save" :loading="savingEntry" @click="saveEntry">Save entry</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Import preview / conflict resolution -->
|
||||
<v-dialog v-model="importDialog" max-width="760" scrollable>
|
||||
<v-card v-if="importPreview">
|
||||
<v-card-title>Import preview — {{ ledgerBook }}</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="ledger-summary mb-3">
|
||||
<div class="ledger-chip"><span class="quiet">New</span><strong>{{ importPreview.summary.new }}</strong></div>
|
||||
<div class="ledger-chip"><span class="quiet">Update</span><strong>{{ importPreview.summary.update }}</strong></div>
|
||||
<div class="ledger-chip"><span class="quiet">Duplicate</span><strong>{{ importPreview.summary.duplicate }}</strong></div>
|
||||
<div class="ledger-chip"><span class="quiet">Errors</span><strong>{{ importPreview.summary.error }}</strong></div>
|
||||
</div>
|
||||
<div class="text-body-2 quiet mb-1">Conflict strategy</div>
|
||||
<v-radio-group v-model="importStrategy" inline hide-details density="compact" class="mb-2">
|
||||
<v-radio label="Skip conflicts" value="skip" />
|
||||
<v-radio label="Merge (update existing)" value="merge" />
|
||||
<v-radio label="Insert as new" value="insert" />
|
||||
</v-radio-group>
|
||||
<div style="overflow-x:auto; max-height:340px">
|
||||
<table class="asset-table">
|
||||
<thead><tr><th>Status</th><th>Date</th><th>Account</th><th class="text-right">Debit</th><th class="text-right">Credit</th><th>Reference</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="r in importPreview.rows" :key="r.index">
|
||||
<td><v-chip size="x-small" :color="statusColor(r.status)" variant="tonal">{{ r.status }}</v-chip><span v-if="r.error" class="quiet text-caption"> · {{ r.error }}</span></td>
|
||||
<td>{{ r.entry.entry_date }}</td>
|
||||
<td>{{ r.entry.account }}</td>
|
||||
<td class="text-right">{{ r.entry.debit ? currency(r.entry.debit) : '' }}</td>
|
||||
<td class="text-right">{{ r.entry.credit ? currency(r.entry.credit) : '' }}</td>
|
||||
<td>{{ r.entry.reference }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="importDialog = false">Cancel</v-btn>
|
||||
<v-btn color="primary" prepend-icon="mdi-database-import" :loading="importing" @click="commitImport">Import</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Per-entry change history (from the audit log) -->
|
||||
<v-dialog v-model="historyDialog" max-width="640" scrollable>
|
||||
<v-card>
|
||||
<v-card-title>Change history</v-card-title>
|
||||
<v-card-text>
|
||||
<div v-if="historyLoading" class="quiet text-body-2">Loading…</div>
|
||||
<div v-else-if="!historyRows.length" class="quiet text-body-2">No recorded changes.</div>
|
||||
<div v-for="ev in historyRows" :key="ev.id" class="gl-history-item">
|
||||
<div class="toolbar-row mb-1" style="gap:8px">
|
||||
<v-chip size="x-small" :color="historyColor(ev.action)" variant="tonal">{{ historyLabel(ev.action) }}</v-chip>
|
||||
<span class="text-body-2 font-weight-medium">{{ ev.actor_name || 'System' }}</span>
|
||||
<v-spacer />
|
||||
<span class="quiet text-caption">{{ shortDate(ev.created_at) }}</span>
|
||||
</div>
|
||||
<table v-if="changedFields(ev.before, ev.after).length" class="gl-diff">
|
||||
<tr v-for="(c, i) in changedFields(ev.before, ev.after)" :key="i">
|
||||
<td class="quiet">{{ c.label }}</td>
|
||||
<td class="gl-from">{{ c.from }}</td>
|
||||
<td class="px-2">→</td>
|
||||
<td class="gl-to">{{ c.to }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div v-else class="quiet text-caption">No field changes.</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="historyDialog = false">Close</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="newBookDialog" max-width="560">
|
||||
<v-card>
|
||||
<v-card-title>New book</v-card-title>
|
||||
@@ -178,7 +331,7 @@
|
||||
<script>
|
||||
import DataTable from '../components/DataTable.vue';
|
||||
import { apiRequest, saveBlob } from '../services/api';
|
||||
import { currency } from '../utils/format';
|
||||
import { currency, shortDate } from '../utils/format';
|
||||
import { conventionItems } from '../constants';
|
||||
|
||||
export default {
|
||||
@@ -207,7 +360,22 @@ export default {
|
||||
newBookDialog: false,
|
||||
newBookError: '',
|
||||
creating: false,
|
||||
newBook: this.blankBook()
|
||||
newBook: this.blankBook(),
|
||||
entries: [],
|
||||
entrySummary: { count: 0, debit: 0, credit: 0, balance: 0 },
|
||||
entryDialog: false,
|
||||
entryForm: this.blankEntry(),
|
||||
entryDialogError: '',
|
||||
savingEntry: false,
|
||||
entryError: '',
|
||||
importDialog: false,
|
||||
importPreview: null,
|
||||
importStrategy: 'merge',
|
||||
importing: false,
|
||||
historyDialog: false,
|
||||
historyRows: [],
|
||||
historyLoading: false,
|
||||
glFieldLabels: { entry_date: 'Date', account: 'Account', account_type: 'Type', description: 'Description', reference: 'Reference', debit: 'Debit', credit: 'Credit', external_id: 'External ID', asset_id: 'Asset' }
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -238,6 +406,18 @@ export default {
|
||||
{ key: 'accumulated', label: 'Accum.', format: 'currency' },
|
||||
{ key: 'nbv', label: 'NBV', format: 'currency' }
|
||||
];
|
||||
},
|
||||
entryColumns() {
|
||||
return [
|
||||
{ key: 'entry_date', label: 'Date', format: 'date' },
|
||||
{ key: 'account', label: 'Account' },
|
||||
{ key: 'account_type', label: 'Type' },
|
||||
{ key: 'description', label: 'Description' },
|
||||
{ key: 'reference', label: 'Reference' },
|
||||
{ key: 'debit', label: 'Debit', format: 'currency' },
|
||||
{ key: 'credit', label: 'Credit', format: 'currency' },
|
||||
{ key: 'source', label: 'Source', sortable: false }
|
||||
];
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -324,6 +504,7 @@ export default {
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
}
|
||||
this.loadEntries();
|
||||
},
|
||||
async exportLedger(format) {
|
||||
const blob = await (await this.api(`/api/books/${this.ledgerBook}/ledger/export`, {
|
||||
@@ -331,6 +512,128 @@ export default {
|
||||
body: JSON.stringify({ year: this.ledgerYear, format })
|
||||
})).blob();
|
||||
saveBlob(blob, `deprecore-${this.ledgerBook}-ledger-${this.ledgerYear}.${format}`);
|
||||
},
|
||||
// ---- GL journal entries -------------------------------------------------
|
||||
blankEntry() {
|
||||
return { id: null, entry_date: new Date().toISOString().slice(0, 10), account: '', account_type: '', description: '', reference: '', debit: 0, credit: 0, external_id: '' };
|
||||
},
|
||||
async loadEntries() {
|
||||
if (!this.ledgerBook) return;
|
||||
this.entryError = '';
|
||||
try {
|
||||
const data = await (await this.api(`/api/books/${this.ledgerBook}/gl-entries?year=${this.ledgerYear}`)).json();
|
||||
this.entries = data.entries;
|
||||
this.entrySummary = data.summary;
|
||||
} catch (error) {
|
||||
this.entryError = error.message;
|
||||
}
|
||||
},
|
||||
openEntry(row) {
|
||||
this.entryForm = row ? { ...this.blankEntry(), ...row } : this.blankEntry();
|
||||
this.entryDialogError = '';
|
||||
this.entryDialog = true;
|
||||
},
|
||||
async saveEntry() {
|
||||
this.savingEntry = true;
|
||||
this.entryDialogError = '';
|
||||
this.message = '';
|
||||
try {
|
||||
const method = this.entryForm.id ? 'PUT' : 'POST';
|
||||
const url = this.entryForm.id
|
||||
? `/api/books/${this.ledgerBook}/gl-entries/${this.entryForm.id}`
|
||||
: `/api/books/${this.ledgerBook}/gl-entries`;
|
||||
await this.api(url, { method, body: JSON.stringify(this.entryForm) });
|
||||
this.entryDialog = false;
|
||||
await this.loadEntries();
|
||||
} catch (error) {
|
||||
this.entryDialogError = error.message;
|
||||
} finally {
|
||||
this.savingEntry = false;
|
||||
}
|
||||
},
|
||||
async removeEntry(row) {
|
||||
if (!window.confirm('Delete this GL entry? The change is recorded in the audit trail.')) return;
|
||||
this.entryError = '';
|
||||
try {
|
||||
await this.api(`/api/books/${this.ledgerBook}/gl-entries/${row.id}`, { method: 'DELETE' });
|
||||
await this.loadEntries();
|
||||
} catch (error) {
|
||||
this.entryError = error.message;
|
||||
}
|
||||
},
|
||||
async onImportFile(event) {
|
||||
const file = event.target.files[0];
|
||||
event.target.value = '';
|
||||
if (!file) return;
|
||||
this.entryError = '';
|
||||
this.message = '';
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
const data = await (await this.api(`/api/books/${this.ledgerBook}/gl-entries/import/preview`, { method: 'POST', body: form })).json();
|
||||
this.importPreview = data;
|
||||
this.importStrategy = 'merge';
|
||||
this.importDialog = true;
|
||||
} catch (error) {
|
||||
this.entryError = `Import failed: ${error.message}`;
|
||||
}
|
||||
},
|
||||
async commitImport() {
|
||||
this.importing = true;
|
||||
try {
|
||||
const result = await (await this.api(`/api/books/${this.ledgerBook}/gl-entries/import/commit`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ rows: this.importPreview.rows, strategy: this.importStrategy })
|
||||
})).json();
|
||||
this.importDialog = false;
|
||||
this.message = `Imported: ${result.inserted} new, ${result.updated} updated, ${result.skipped} skipped${result.errors ? `, ${result.errors} error(s)` : ''}.`;
|
||||
await this.loadEntries();
|
||||
} catch (error) {
|
||||
this.entryError = error.message;
|
||||
} finally {
|
||||
this.importing = false;
|
||||
}
|
||||
},
|
||||
async exportEntries(format) {
|
||||
const blob = await (await this.api(`/api/books/${this.ledgerBook}/gl-entries/export?year=${this.ledgerYear}&format=${format}`)).blob();
|
||||
saveBlob(blob, `deprecore-${this.ledgerBook}-gl-entries.${format}`);
|
||||
},
|
||||
statusColor(status) {
|
||||
return { new: 'success', update: 'info', duplicate: 'warning', error: 'error' }[status] || undefined;
|
||||
},
|
||||
shortDate,
|
||||
async openHistory(row) {
|
||||
this.historyRows = [];
|
||||
this.historyLoading = true;
|
||||
this.historyDialog = true;
|
||||
try {
|
||||
const data = await (await this.api(`/api/books/${this.ledgerBook}/gl-entries/${row.id}/history`)).json();
|
||||
this.historyRows = data.history;
|
||||
} catch (error) {
|
||||
this.entryError = error.message;
|
||||
this.historyDialog = false;
|
||||
} finally {
|
||||
this.historyLoading = false;
|
||||
}
|
||||
},
|
||||
// Field-level diff between an audit event's before/after snapshots.
|
||||
changedFields(before, after) {
|
||||
const norm = (v) => (v === null || v === undefined || v === '' ? '' : String(v));
|
||||
const b = before || {};
|
||||
const a = after || {};
|
||||
const out = [];
|
||||
for (const [key, label] of Object.entries(this.glFieldLabels)) {
|
||||
const from = norm(b[key]);
|
||||
const to = norm(a[key]);
|
||||
if (from !== to) out.push({ label, from: from || '—', to: to || '—' });
|
||||
}
|
||||
return out;
|
||||
},
|
||||
historyLabel(action) {
|
||||
return { create: 'Created', update: 'Updated', delete: 'Deleted' }[action] || action;
|
||||
},
|
||||
historyColor(action) {
|
||||
return { create: 'success', update: 'info', delete: 'error' }[action] || undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -354,4 +657,26 @@ export default {
|
||||
.ledger-chip strong {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
.gl-history-item {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
.gl-history-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.gl-diff {
|
||||
font-size: 0.85rem;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.gl-diff td {
|
||||
padding: 2px 6px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.gl-from {
|
||||
color: rgb(var(--v-theme-error));
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.gl-to {
|
||||
color: rgb(var(--v-theme-success));
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user