feat: categorizar transações no preview de importação
Select de categoria em cada linha da prévia; category_id salvo no BulkInsert. Duplicatas ficam desabilitadas. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
@@ -21,6 +21,7 @@ type ImportRow struct {
|
|||||||
Amount float64 `json:"amount"`
|
Amount float64 `json:"amount"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
CategoryID *int `json:"category_id,omitempty"`
|
||||||
ExternalID string `json:"external_id,omitempty"` // FITID from OFX
|
ExternalID string `json:"external_id,omitempty"` // FITID from OFX
|
||||||
IsDuplicate bool `json:"is_duplicate"`
|
IsDuplicate bool `json:"is_duplicate"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,9 +63,9 @@ func (r *transactionRepo) BulkInsert(ctx context.Context, rows []model.ImportRow
|
|||||||
extID = &row.ExternalID
|
extID = &row.ExternalID
|
||||||
}
|
}
|
||||||
_, err := tx.Exec(ctx, `
|
_, err := tx.Exec(ctx, `
|
||||||
INSERT INTO transactions (date, amount, description, type, source, external_id)
|
INSERT INTO transactions (date, amount, description, type, source, external_id, category_id)
|
||||||
VALUES ($1, $2, $3, $4, 'import', $5)`,
|
VALUES ($1, $2, $3, $4, 'import', $5, $6)`,
|
||||||
row.Date, row.Amount, row.Description, row.Type, extID)
|
row.Date, row.Amount, row.Description, row.Type, extID, row.CategoryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface ImportRow {
|
|||||||
amount: number
|
amount: number
|
||||||
description: string
|
description: string
|
||||||
type: string
|
type: string
|
||||||
|
category_id?: number | null
|
||||||
external_id?: string
|
external_id?: string
|
||||||
is_duplicate: boolean
|
is_duplicate: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
import { useImportStore, type CSVMapping } from '@/stores/import'
|
import { useImportStore, type CSVMapping } from '@/stores/import'
|
||||||
|
import { useCategoriesStore } from '@/stores/categories'
|
||||||
import NeonPanel from '@/components/NeonPanel.vue'
|
import NeonPanel from '@/components/NeonPanel.vue'
|
||||||
|
|
||||||
const store = useImportStore()
|
const store = useImportStore()
|
||||||
|
const catStore = useCategoriesStore()
|
||||||
|
onMounted(() => catStore.fetchAll())
|
||||||
|
|
||||||
const fileInput = ref<HTMLInputElement | null>(null)
|
const fileInput = ref<HTMLInputElement | null>(null)
|
||||||
const selectedFile = ref<File | null>(null)
|
const selectedFile = ref<File | null>(null)
|
||||||
@@ -192,6 +195,7 @@ function fmt(amount: number) {
|
|||||||
<th class="fc-pixel">DESCRIÇÃO</th>
|
<th class="fc-pixel">DESCRIÇÃO</th>
|
||||||
<th class="fc-pixel">VALOR</th>
|
<th class="fc-pixel">VALOR</th>
|
||||||
<th class="fc-pixel">TIPO</th>
|
<th class="fc-pixel">TIPO</th>
|
||||||
|
<th class="fc-pixel">CATEGORIA</th>
|
||||||
<th class="fc-pixel">STATUS</th>
|
<th class="fc-pixel">STATUS</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -203,6 +207,16 @@ function fmt(amount: number) {
|
|||||||
{{ fmt(row.amount) }}
|
{{ fmt(row.amount) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="fc-mono">{{ row.type === 'income' ? 'Receita' : 'Gasto' }}</td>
|
<td class="fc-mono">{{ row.type === 'income' ? 'Receita' : 'Gasto' }}</td>
|
||||||
|
<td>
|
||||||
|
<select
|
||||||
|
v-model="row.category_id"
|
||||||
|
class="fc-select fc-import-cat-select"
|
||||||
|
:disabled="row.is_duplicate"
|
||||||
|
>
|
||||||
|
<option :value="null">—</option>
|
||||||
|
<option v-for="c in catStore.categories" :key="c.id" :value="c.id">{{ c.name }}</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="row.is_duplicate" class="fc-pixel fc-import-status fc-import-status--dup">DUP</span>
|
<span v-if="row.is_duplicate" class="fc-pixel fc-import-status fc-import-status--dup">DUP</span>
|
||||||
<span v-else class="fc-pixel fc-import-status fc-import-status--new">NOVO</span>
|
<span v-else class="fc-pixel fc-import-status fc-import-status--new">NOVO</span>
|
||||||
@@ -334,6 +348,13 @@ function fmt(amount: number) {
|
|||||||
.fc-import-status--new { background: rgba(57,255,122,.15); color: var(--fc-green); }
|
.fc-import-status--new { background: rgba(57,255,122,.15); color: var(--fc-green); }
|
||||||
.fc-import-status--dup { background: rgba(255,216,77,.15); color: var(--fc-gold); }
|
.fc-import-status--dup { background: rgba(255,216,77,.15); color: var(--fc-gold); }
|
||||||
|
|
||||||
|
.fc-import-cat-select {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
min-width: 110px;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
.fc-import-confirm {
|
.fc-import-confirm {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--fc-space-3);
|
gap: var(--fc-space-3);
|
||||||
|
|||||||
Reference in New Issue
Block a user