194 lines
8.0 KiB
Vue
194 lines
8.0 KiB
Vue
<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 zone’s §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>
|