Files
MixedAssets/src/components/DepreciationZoneSettings.vue
2026-06-05 05:55:43 -05:00

194 lines
8.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<v-card class="span-12 panel-pad">
<div class="toolbar-row mb-3">
<div>
<h2 class="section-title">Depreciation zones</h2>
<p class="quiet text-body-2">
Special-allowance zones (e.g. New York Liberty Zone, Enterprise/Empowerment Zone). Assign one to an asset and the engine
applies the zones §168 allowance and/or increased §179 limit to the federal book for assets placed in service within the
date window.
</p>
</div>
<v-spacer />
<v-btn color="primary" prepend-icon="mdi-plus" @click="openNew">New zone</v-btn>
</div>
<DataTable
:columns="columns"
:rows="zones"
row-key="code"
:page-size="10"
has-actions
searchable
search-placeholder="Filter zones"
empty-text="No depreciation zones."
>
<template #cell-name="{ row }">
<span class="font-weight-bold">{{ row.name }}</span>
<v-chip v-if="!row.enabled" size="x-small" variant="tonal" class="ml-2">Disabled</v-chip>
</template>
<template #cell-allowance_percent="{ row }">{{ row.allowance_percent }}%</template>
<template #cell-section_179_increase="{ row }">{{ row.section_179_increase ? `+$${Number(row.section_179_increase).toLocaleString()}` : '—' }}</template>
<template #cell-window="{ row }">{{ row.pis_start || '—' }} {{ row.pis_end || '—' }}</template>
<template #actions="{ row }">
<v-btn icon="mdi-pencil" size="small" variant="text" @click="openEdit(row)" />
<v-btn icon="mdi-delete-outline" size="small" variant="text" color="error" @click="confirmDelete(row)" />
</template>
</DataTable>
<v-alert v-if="error" type="error" density="compact" class="mt-3">{{ error }}</v-alert>
<v-dialog v-model="dialog" max-width="640" scrollable>
<v-card>
<v-card-title>{{ form.exists ? `Edit zone · ${form.name}` : 'New depreciation zone' }}</v-card-title>
<v-card-text>
<div class="form-grid">
<v-text-field v-model="form.name" label="Zone name *" />
<v-text-field v-if="!form.exists" v-model="form.code" label="Code (optional)" placeholder="auto from name" />
<v-text-field v-model.number="form.allowance_percent" type="number" label="§168 special allowance %" suffix="%" />
<v-switch v-model="form.enabled" label="Enabled" color="primary" density="compact" hide-details />
<v-text-field v-model="form.pis_start" type="date" label="Placed-in-service start" />
<v-text-field v-model="form.pis_end" type="date" label="Placed-in-service end" />
<v-text-field v-model="form.real_property_end" type="date" label="Real-property PIS end (optional)" />
<v-text-field v-model.number="form.max_recovery_years" type="number" label="Max recovery years (optional)" />
<v-text-field v-model.number="form.leasehold_life_years" type="number" label="Leasehold improvement life (yrs, optional)" />
<v-text-field v-model.number="form.section_179_increase" type="number" label="§179 limit increase ($)" prefix="$" hint="Enterprise/empowerment zones, e.g. 35000" persistent-hint />
<v-text-field v-model.number="form.section_179_cost_factor" type="number" step="0.01" label="§179 cost factor for phaseout" hint="Share of cost counted toward the threshold (e.g. 0.5)" persistent-hint />
</div>
<v-textarea v-model="form.notes" class="mt-2" label="Notes" rows="3" />
<v-alert v-if="dialogError" type="error" density="compact" class="mt-2">{{ dialogError }}</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="dialog = false">Cancel</v-btn>
<v-btn color="primary" prepend-icon="mdi-content-save" :loading="saving" :disabled="!form.name.trim()" @click="save">Save zone</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="deleteDialog" max-width="460">
<v-card>
<v-card-title class="text-error">Delete zone</v-card-title>
<v-card-text>
<p>Delete <strong>{{ deleteTarget?.name }}</strong>?</p>
<v-alert v-if="deleteTarget && deleteTarget.asset_count" type="warning" variant="tonal" density="comfortable" class="mt-2">
{{ deleteTarget.asset_count }} asset(s) reference this zone they keep their value, but the special allowance stops applying.
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="deleteDialog = false">Cancel</v-btn>
<v-btn color="error" prepend-icon="mdi-delete-outline" @click="performDelete">Delete</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>
</template>
<script>
import DataTable from './DataTable.vue';
import { apiRequest } from '../services/api';
function blankForm() {
return {
exists: false, code: '', name: '', allowance_percent: 0, enabled: true,
pis_start: '', pis_end: '', real_property_end: '', max_recovery_years: null, leasehold_life_years: null,
section_179_increase: 0, section_179_cost_factor: 1, notes: ''
};
}
export default {
components: { DataTable },
props: {
token: { type: String, required: true }
},
emits: ['updated'],
data() {
return {
zones: [],
dialog: false,
deleteDialog: false,
deleteTarget: null,
form: blankForm(),
saving: false,
error: '',
dialogError: '',
columns: [
{ key: 'name', label: 'Zone' },
{ key: 'allowance_percent', label: '§168 allowance' },
{ key: 'section_179_increase', label: '§179 increase', sortable: false },
{ key: 'window', label: 'Placed-in-service window', sortable: false },
{ key: 'asset_count', label: 'Assets', format: 'number' }
]
};
},
mounted() {
this.load();
},
methods: {
async api(path, options = {}) {
return apiRequest(path, this.token, options);
},
async load() {
this.error = '';
try {
const data = await (await this.api('/api/depreciation-zones')).json();
this.zones = data.zones;
} catch (error) {
this.error = error.message;
}
},
openNew() {
this.form = blankForm();
this.dialogError = '';
this.dialog = true;
},
openEdit(row) {
this.form = {
exists: true, code: row.code, name: row.name, allowance_percent: row.allowance_percent, enabled: row.enabled,
pis_start: row.pis_start || '', pis_end: row.pis_end || '', real_property_end: row.real_property_end || '',
max_recovery_years: row.max_recovery_years, leasehold_life_years: row.leasehold_life_years,
section_179_increase: row.section_179_increase || 0,
section_179_cost_factor: row.section_179_cost_factor === null || row.section_179_cost_factor === undefined ? 1 : row.section_179_cost_factor,
notes: row.notes || ''
};
this.dialogError = '';
this.dialog = true;
},
async save() {
if (!this.form.name.trim()) return;
this.saving = true;
this.dialogError = '';
try {
const method = this.form.exists ? 'PUT' : 'POST';
const url = this.form.exists ? `/api/depreciation-zones/${this.form.code}` : '/api/depreciation-zones';
await this.api(url, { method, body: JSON.stringify(this.form) });
this.dialog = false;
await this.load();
this.$emit('updated');
} catch (error) {
this.dialogError = error.message;
} finally {
this.saving = false;
}
},
confirmDelete(row) {
this.deleteTarget = row;
this.deleteDialog = true;
},
async performDelete() {
const target = this.deleteTarget;
this.deleteDialog = false;
this.deleteTarget = null;
if (!target) return;
try {
await this.api(`/api/depreciation-zones/${target.code}`, { method: 'DELETE' });
await this.load();
this.$emit('updated');
} catch (error) {
this.error = error.message;
}
}
}
};
</script>