mitlist/fe/src/components/list-detail/OcrDialog.vue
mohamad 7ffeae1476 feat: Add new components for cost summary, expenses, and item management
This commit introduces several new components to enhance the list detail functionality:

- **CostSummaryDialog.vue**: A modal for displaying cost summaries, including total costs, user balances, and a detailed breakdown of expenses.
- **ExpenseSection.vue**: A section for managing and displaying expenses, featuring loading states, error handling, and collapsible item details.
- **ItemsList.vue**: A component for rendering and managing a list of items with drag-and-drop functionality, including a new item input field.
- **ListItem.vue**: A detailed item component that supports editing, deleting, and displaying item statuses.
- **OcrDialog.vue**: A modal for handling OCR file uploads and displaying extracted items.
- **SettleShareModal.vue**: A modal for settling shares among users, allowing input of settlement amounts.
- **Error handling utility**: A new utility function for extracting user-friendly error messages from API responses.

These additions aim to improve user interaction and streamline the management of costs and expenses within the application.
2025-06-09 22:55:37 +02:00

114 lines
4.3 KiB
Vue

<template>
<VModal :model-value="modelValue" :title="$t('listDetailPage.modals.ocr.title')"
@update:modelValue="$emit('update:modelValue', $event)">
<template #default>
<div v-if="ocrLoading" class="text-center">
<VSpinner :label="$t('listDetailPage.loading.ocrProcessing')" />
</div>
<VList v-else-if="ocrItems.length > 0">
<VListItem v-for="(ocrItem, index) in ocrItems" :key="index">
<div class="flex items-center gap-2">
<VInput type="text" v-model="ocrItem.name" class="flex-grow" required />
<VButton variant="danger" size="sm" :icon-only="true" iconLeft="trash"
@click="ocrItems.splice(index, 1)" />
</div>
</VListItem>
</VList>
<VFormField v-else :label="$t('listDetailPage.modals.ocr.uploadLabel')"
:error-message="ocrError || undefined">
<VInput type="file" id="ocrFile" accept="image/*" @change="handleOcrFileUpload" ref="ocrFileInputRef"
:model-value="''" />
</VFormField>
</template>
<template #footer>
<VButton variant="neutral" @click="$emit('update:modelValue', false)">{{ $t('listDetailPage.buttons.cancel')
}}</VButton>
<VButton v-if="ocrItems.length > 0" type="button" variant="primary" @click="confirmAddItems"
:disabled="isAdding">
<VSpinner v-if="isAdding" size="sm" />
{{ $t('listDetailPage.buttons.addItems') }}
</VButton>
</template>
</VModal>
</template>
<script setup lang="ts">
import { ref, watch, defineProps, defineEmits } from 'vue';
import { useI18n } from 'vue-i18n';
import { apiClient, API_ENDPOINTS } from '@/services/api';
import { getApiErrorMessage } from '@/utils/errors';
import VModal from '@/components/valerie/VModal.vue';
import VSpinner from '@/components/valerie/VSpinner.vue';
import VList from '@/components/valerie/VList.vue';
import VListItem from '@/components/valerie/VListItem.vue';
import VInput from '@/components/valerie/VInput.vue';
import VButton from '@/components/valerie/VButton.vue';
import VFormField from '@/components/valerie/VFormField.vue';
const props = defineProps({
modelValue: {
type: Boolean,
required: true,
},
isAdding: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:modelValue', 'add-items']);
const { t } = useI18n();
const ocrLoading = ref(false);
const ocrItems = ref<{ name: string }[]>([]);
const ocrError = ref<string | null>(null);
const ocrFileInputRef = ref<InstanceType<typeof VInput> | null>(null);
const handleOcrFileUpload = (event: Event) => {
const target = event.target as HTMLInputElement;
if (target.files && target.files.length > 0) {
handleOcrUpload(target.files[0]);
}
};
const handleOcrUpload = async (file: File) => {
if (!file) return;
ocrLoading.value = true;
ocrError.value = null;
ocrItems.value = [];
try {
const formData = new FormData();
formData.append('image_file', file);
const response = await apiClient.post(API_ENDPOINTS.OCR.PROCESS, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
ocrItems.value = response.data.extracted_items
.map((nameStr: string) => ({ name: nameStr.trim() }))
.filter((item: { name: string }) => item.name);
if (ocrItems.value.length === 0) {
ocrError.value = t('listDetailPage.errors.ocrNoItems');
}
} catch (err) {
ocrError.value = getApiErrorMessage(err, 'listDetailPage.errors.ocrFailed', t);
} finally {
ocrLoading.value = false;
// Reset file input
if (ocrFileInputRef.value?.$el) {
const input = ocrFileInputRef.value.$el.querySelector ? ocrFileInputRef.value.$el.querySelector('input') : ocrFileInputRef.value.$el;
if (input) input.value = '';
}
}
};
const confirmAddItems = () => {
emit('add-items', ocrItems.value);
};
watch(() => props.modelValue, (newVal) => {
if (newVal) {
ocrItems.value = [];
ocrError.value = null;
}
});
</script>