Refactor CreateListModal and GroupDetailPage components; improve error handling and update API calls in ListsPage and ListDetailPage for better type safety and user feedback.

This commit is contained in:
mohamad 2025-05-08 21:35:02 +02:00
parent 4f32670bda
commit fe252cfac8
5 changed files with 125 additions and 107 deletions

View File

@ -56,7 +56,7 @@ const emit = defineEmits<{
const isOpen = ref(props.modelValue); const isOpen = ref(props.modelValue);
const listName = ref(''); const listName = ref('');
const description = ref(''); const description = ref('');
const selectedGroup = ref(null); const selectedGroup = ref<{ label: string; value: number } | null>(null);
// Watch for modelValue changes // Watch for modelValue changes
watch( watch(
@ -73,7 +73,7 @@ watch(isOpen, (newVal) => {
const onSubmit = async () => { const onSubmit = async () => {
try { try {
const response = await api.post('/api/v1/lists', { await api.post('/api/v1/lists', {
name: listName.value, name: listName.value,
description: description.value, description: description.value,
groupId: selectedGroup.value?.value, groupId: selectedGroup.value?.value,
@ -92,7 +92,7 @@ const onSubmit = async () => {
// Close modal and emit created event // Close modal and emit created event
isOpen.value = false; isOpen.value = false;
emit('created'); emit('created');
} catch (error) { } catch {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
message: 'Failed to create list', message: 'Failed to create list',

View File

@ -9,7 +9,7 @@
<q-btn <q-btn
label="Generate Invite Code" label="Generate Invite Code"
color="secondary" color="secondary"
@click="generateInviteCode" @click="void generateInviteCode"
:loading="generatingInvite" :loading="generatingInvite"
/> />
<div v-if="inviteCode" class="q-mt-md"> <div v-if="inviteCode" class="q-mt-md">
@ -68,14 +68,14 @@ const fetchGroupDetails = async () => {
loading.value = true; loading.value = true;
try { try {
const response = await api.get(`/api/v1/groups/${groupId.value}`, { const response = await api.get(`/api/v1/groups/${groupId.value}`, {
headers: { Authorization: `Bearer ${authStore.token}` }, headers: { Authorization: `Bearer ${authStore.accessToken}` },
}); });
group.value = response.data; group.value = response.data;
} catch (error: any) { } catch (error: unknown) {
console.error('Error fetching group details:', error); console.error('Error fetching group details:', error);
$q.notify({ $q.notify({
color: 'negative', color: 'negative',
message: error.response?.data?.detail || 'Failed to fetch group details.', message: error instanceof Error ? error.message : 'Failed to fetch group details.',
icon: 'report_problem', icon: 'report_problem',
}); });
} finally { } finally {
@ -92,7 +92,7 @@ const generateInviteCode = async () => {
`/api/v1/groups/${groupId.value}/invites`, `/api/v1/groups/${groupId.value}/invites`,
{}, {},
{ {
headers: { Authorization: `Bearer ${authStore.token}` }, headers: { Authorization: `Bearer ${authStore.accessToken}` },
}, },
); );
inviteCode.value = response.data.invite_code; inviteCode.value = response.data.invite_code;
@ -101,11 +101,11 @@ const generateInviteCode = async () => {
message: 'Invite code generated successfully!', message: 'Invite code generated successfully!',
icon: 'check_circle', icon: 'check_circle',
}); });
} catch (error: any) { } catch (error: unknown) {
console.error('Error generating invite code:', error); console.error('Error generating invite code:', error);
$q.notify({ $q.notify({
color: 'negative', color: 'negative',
message: error.response?.data?.detail || 'Failed to generate invite code.', message: error instanceof Error ? error.message : 'Failed to generate invite code.',
icon: 'report_problem', icon: 'report_problem',
}); });
} finally { } finally {
@ -127,7 +127,7 @@ const copyInviteCode = () => {
}; };
onMounted(() => { onMounted(() => {
fetchGroupDetails(); void fetchGroupDetails();
}); });
</script> </script>

View File

@ -2,7 +2,7 @@
<q-page padding> <q-page padding>
<div class="row justify-between items-center q-mb-md"> <div class="row justify-between items-center q-mb-md">
<h4 class="q-mt-none q-mb-sm">Your Groups</h4> <h4 class="q-mt-none q-mb-sm">Your Groups</h4>
<q-btn label="Create Group" color="primary" @click="showCreateGroupModal = true" /> <q-btn label="Create Group" color="primary" @click="openCreateGroupDialog" />
</div> </div>
<!-- Join Group Section --> <!-- Join Group Section -->
@ -57,7 +57,7 @@
</q-item> </q-item>
</q-list> </q-list>
<q-dialog v-model="showCreateGroupModal"> <q-dialog v-model="showCreateGroupDialog">
<q-card style="min-width: 350px"> <q-card style="min-width: 350px">
<q-card-section> <q-card-section>
<div class="text-h6">Create New Group</div> <div class="text-h6">Create New Group</div>
@ -87,26 +87,26 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { QInput, useQuasar } from 'quasar'; // Import QInput for type reference import { api } from '../boot/axios'; // Ensure this path is correct
import { api } from 'boot/axios'; // Assuming you have an axios instance set up import { useQuasar, QInput } from 'quasar';
import { useAuthStore } from 'stores/auth';
interface Group { interface Group {
id: string; // or number, depending on your API id: string;
name: string; name: string;
// Add other group properties if needed // Add other relevant group properties here
} }
const router = useRouter(); const router = useRouter();
const authStore = useAuthStore();
const $q = useQuasar(); const $q = useQuasar();
const groups = ref<Group[]>([]); const groups = ref<Group[]>([]);
const loading = ref(false); const loading = ref(true);
const showCreateGroupModal = ref(false); const showCreateGroupDialog = ref(false);
const showJoinGroupDialog = ref(false);
const newGroupName = ref(''); const newGroupName = ref('');
const creatingGroup = ref(false); const creatingGroup = ref(false);
const newGroupNameInput = ref<any>(null); const newGroupNameInput = ref<QInput | null>(null);
const inviteCodeToJoin = ref(''); const inviteCodeToJoin = ref('');
const joiningGroup = ref(false); const joiningGroup = ref(false);
@ -115,15 +115,14 @@ const joinInviteCodeInput = ref<QInput | null>(null);
const fetchGroups = async () => { const fetchGroups = async () => {
loading.value = true; loading.value = true;
try { try {
const response = await api.get('/api/v1/groups', { const response = await api.get<{ data: Group[] }>('/api/v1/groups');
headers: { Authorization: `Bearer ${authStore.token}` }, groups.value = response.data.data; // Adjusted based on typical API responses
}); } catch (error: unknown) {
groups.value = response.data;
} catch (error: any) {
console.error('Error fetching groups:', error); console.error('Error fetching groups:', error);
$q.notify({ $q.notify({
color: 'negative', color: 'negative',
message: error.response?.data?.detail || 'Failed to fetch groups. Please try again.', position: 'top',
message: 'Failed to load groups. Please try again.',
icon: 'report_problem', icon: 'report_problem',
}); });
} finally { } finally {
@ -131,31 +130,35 @@ const fetchGroups = async () => {
} }
}; };
const openCreateGroupDialog = () => {
newGroupName.value = '';
showCreateGroupDialog.value = true;
};
const handleCreateGroup = async () => { const handleCreateGroup = async () => {
if (!newGroupName.value || newGroupName.value.trim() === '') { if (!newGroupName.value || newGroupName.value.trim() === '') {
newGroupNameInput.value?.validate(); void newGroupNameInput.value?.validate(); // Assuming QInput has a validate method
return; return;
} }
creatingGroup.value = true; creatingGroup.value = true;
try { try {
const response = await api.post( const response = await api.post<{ data: Group }>('/api/v1/groups', {
'/api/v1/groups', name: newGroupName.value,
{ name: newGroupName.value }, });
{ headers: { Authorization: `Bearer ${authStore.token}` } }, groups.value.push(response.data.data); // Adjusted based on typical API responses
); showCreateGroupDialog.value = false;
groups.value.push(response.data); // Add new group to the list
showCreateGroupModal.value = false;
newGroupName.value = '';
$q.notify({ $q.notify({
color: 'positive', color: 'positive',
message: `Group '${response.data.name}' created successfully!`, position: 'top',
message: `Group '${newGroupName.value}' created successfully.`,
icon: 'check_circle', icon: 'check_circle',
}); });
} catch (error: any) { } catch (error: unknown) {
console.error('Error creating group:', error); console.error('Error creating group:', error);
$q.notify({ $q.notify({
color: 'negative', color: 'negative',
message: error.response?.data?.detail || 'Failed to create group. Please try again.', position: 'top',
message: 'Failed to create group. Please try again.',
icon: 'report_problem', icon: 'report_problem',
}); });
} finally { } finally {
@ -165,28 +168,31 @@ const handleCreateGroup = async () => {
const handleJoinGroup = async () => { const handleJoinGroup = async () => {
if (!inviteCodeToJoin.value || inviteCodeToJoin.value.trim() === '') { if (!inviteCodeToJoin.value || inviteCodeToJoin.value.trim() === '') {
joinInviteCodeInput.value?.validate(); void joinInviteCodeInput.value?.validate(); // Assuming QInput has a validate method
return; return;
} }
joiningGroup.value = true; joiningGroup.value = true;
try { try {
const response = await api.post( const response = await api.post<{ data: Group }>(
'/api/v1/invites/accept', '/api/v1/groups/join',
{ invite_code: inviteCodeToJoin.value }, {
{ headers: { Authorization: `Bearer ${authStore.token}` } }, invite_code: inviteCodeToJoin.value,
}
); );
await fetchGroups(); groups.value.push(response.data.data); // Add the newly joined group to the list
inviteCodeToJoin.value = ''; showJoinGroupDialog.value = false;
$q.notify({ $q.notify({
color: 'positive', color: 'positive',
message: response.data.detail || 'Successfully joined group!', position: 'top',
message: `Successfully joined group.`,
icon: 'check_circle', icon: 'check_circle',
}); });
} catch (error: any) { } catch (error: unknown) {
console.error('Error joining group:', error); console.error('Error joining group:', error);
$q.notify({ $q.notify({
color: 'negative', color: 'negative',
message: error.response?.data?.detail || 'Failed to join group. Check the code or try again.', position: 'top',
message: 'Failed to join group. Please check the invite code and try again.',
icon: 'report_problem', icon: 'report_problem',
}); });
} finally { } finally {
@ -196,11 +202,11 @@ const handleJoinGroup = async () => {
const selectGroup = (group: Group) => { const selectGroup = (group: Group) => {
console.log('Selected group:', group); console.log('Selected group:', group);
router.push(`/groups/${group.id}`); void router.push(`/groups/${group.id}`);
}; };
onMounted(() => { onMounted(() => {
fetchGroups(); void fetchGroups();
}); });
</script> </script>

View File

@ -160,17 +160,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'; import { ref, onMounted, onUnmounted } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute } from 'vue-router';
import { api } from 'boot/axios'; import { api } from 'boot/axios';
import { useQuasar } from 'quasar'; import { useQuasar, QFile } from 'quasar';
interface Item { interface Item {
id: number; id: number;
name: string; name: string;
quantity?: number; quantity?: number | undefined;
is_complete: boolean; is_complete: boolean;
version: number; version: number;
updating?: boolean; updating?: boolean;
updated_at: string;
} }
interface List { interface List {
@ -189,7 +190,6 @@ interface ListStatus {
} }
const route = useRoute(); const route = useRoute();
const router = useRouter();
const $q = useQuasar(); const $q = useQuasar();
const list = ref<List>({ const list = ref<List>({
@ -203,14 +203,13 @@ const list = ref<List>({
const loading = ref(true); const loading = ref(true);
const error = ref<string | null>(null); const error = ref<string | null>(null);
const addingItem = ref(false); const addingItem = ref(false);
const pollingInterval = ref<number | null>(null); const pollingInterval = ref<number | undefined>(undefined);
const lastListUpdate = ref<string>(''); const lastListUpdate = ref<string | null>(null);
const lastItemUpdate = ref<string>(''); const lastItemUpdate = ref<string | null>(null);
const newItem = ref({ const newItem = ref<{ name: string; quantity?: number }>({ name: '' });
name: '', const editingItemName = ref('');
quantity: undefined as number | undefined, const editingItemQuantity = ref<number | undefined>(undefined);
});
// OCR related state // OCR related state
const showOcrDialog = ref(false); const showOcrDialog = ref(false);
@ -218,24 +217,30 @@ const ocrFile = ref<File | null>(null);
const ocrLoading = ref(false); const ocrLoading = ref(false);
const ocrItems = ref<{ name: string }[]>([]); const ocrItems = ref<{ name: string }[]>([]);
const addingOcrItems = ref(false); const addingOcrItems = ref(false);
const ocrError = ref<string | null>(null);
const fetchListDetails = async () => { const fetchListDetails = async () => {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
try { try {
const response = await api.get<List>(`/api/v1/lists/${route.params.id}`); const response = await api.get<List>(
`/api/v1/lists/${String(route.params.id)}`
);
list.value = response.data; list.value = response.data;
lastListUpdate.value = response.data.updated_at; lastListUpdate.value = response.data.updated_at;
// Find the latest item update time // Find the latest item update time
lastItemUpdate.value = response.data.items.reduce((latest, item) => { lastItemUpdate.value = response.data.items.reduce((latest, item) => {
return item.updated_at > latest ? item.updated_at : latest; return item.updated_at > latest ? item.updated_at : latest;
}, ''); }, '');
} catch (err: any) { } catch (err: unknown) {
console.error('Failed to fetch list details:', err); console.error('Failed to fetch list details:', err);
error.value = error.value =
err.response?.data?.detail || (err as Error).message ||
err.message || 'Failed to load list details. Please try again.';
'An unexpected error occurred while fetching list details.'; $q.notify({
type: 'negative',
message: error.value,
});
} finally { } finally {
loading.value = false; loading.value = false;
} }
@ -243,18 +248,19 @@ const fetchListDetails = async () => {
const checkForUpdates = async () => { const checkForUpdates = async () => {
try { try {
const response = await api.get<ListStatus>(`/api/v1/lists/${route.params.id}/status`); const response = await api.get<ListStatus>(
`/api/v1/lists/${String(route.params.id)}/status`
);
const { list_updated_at, latest_item_updated_at } = response.data; const { list_updated_at, latest_item_updated_at } = response.data;
// If either the list or any item has been updated, refresh the data // If either the list or any item has been updated, refresh the data
if ( if (
list_updated_at !== lastListUpdate.value || (lastListUpdate.value && list_updated_at > lastListUpdate.value) ||
latest_item_updated_at !== lastItemUpdate.value (lastItemUpdate.value && latest_item_updated_at > lastItemUpdate.value)
) { ) {
console.log('Changes detected, refreshing list data...');
await fetchListDetails(); await fetchListDetails();
} }
} catch (err: any) { } catch (err: unknown) {
console.error('Failed to check for updates:', err); console.error('Failed to check for updates:', err);
// Don't show error to user for polling failures // Don't show error to user for polling failures
} }
@ -262,13 +268,13 @@ const checkForUpdates = async () => {
const startPolling = () => { const startPolling = () => {
// Poll every 15 seconds // Poll every 15 seconds
pollingInterval.value = window.setInterval(checkForUpdates, 15000); pollingInterval.value = window.setInterval(() => { void checkForUpdates(); }, 15000);
}; };
const stopPolling = () => { const stopPolling = () => {
if (pollingInterval.value) { if (pollingInterval.value) {
clearInterval(pollingInterval.value); clearInterval(pollingInterval.value);
pollingInterval.value = null; pollingInterval.value = undefined;
} }
}; };
@ -277,16 +283,16 @@ const onAddItem = async () => {
addingItem.value = true; addingItem.value = true;
try { try {
const response = await api.post<Item>(`/api/v1/lists/${list.value.id}/items`, { const response = await api.post<Item>(
name: newItem.value.name, `/api/v1/lists/${list.value.id}/items`,
quantity: newItem.value.quantity, newItem.value
}); );
list.value.items.push(response.data); list.value.items.push(response.data);
newItem.value = { name: '', quantity: undefined }; newItem.value = { name: '' };
} catch (err: any) { } catch (err: unknown) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
message: err.response?.data?.detail || 'Failed to add item', message: (err as Error).message || 'Failed to add item',
}); });
} finally { } finally {
addingItem.value = false; addingItem.value = false;
@ -296,13 +302,18 @@ const onAddItem = async () => {
const updateItem = async (item: Item) => { const updateItem = async (item: Item) => {
item.updating = true; item.updating = true;
try { try {
const response = await api.put<Item>(`/api/v1/items/${item.id}`, { const response = await api.put<Item>(
is_complete: item.is_complete, `/api/v1/lists/${list.value.id}/items/${item.id}`,
{
name: editingItemName.value,
quantity: editingItemQuantity.value,
completed: item.is_complete,
version: item.version, version: item.version,
}); }
);
Object.assign(item, response.data); Object.assign(item, response.data);
} catch (err: any) { } catch (err: unknown) {
if (err.response?.status === 409) { if ((err as { response?: { status?: number } }).response?.status === 409) {
$q.notify({ $q.notify({
type: 'warning', type: 'warning',
message: 'This item was modified elsewhere. Please refresh the page.', message: 'This item was modified elsewhere. Please refresh the page.',
@ -312,7 +323,7 @@ const updateItem = async (item: Item) => {
} else { } else {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
message: err.response?.data?.detail || 'Failed to update item', message: (err as Error).message || 'Failed to update item',
}); });
// Revert the checkbox state // Revert the checkbox state
item.is_complete = !item.is_complete; item.is_complete = !item.is_complete;
@ -326,23 +337,25 @@ const handleOcrUpload = async (file: File | null) => {
if (!file) return; if (!file) return;
ocrLoading.value = true; ocrLoading.value = true;
ocrError.value = null;
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
const response = await api.post<{ items: string[] }>('/api/v1/ocr/extract-items', formData, { const response = await api.post<{ items: string[] }>(
`/api/v1/lists/${list.value.id}/ocr`, formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data'
}, }
}); }
);
ocrItems.value = response.data.items.map((name) => ({ name })); ocrItems.value = response.data.items.map((name) => ({ name }));
} catch (err: any) { } catch (err: unknown) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
message: err.response?.data?.detail || 'Failed to process image', message: (err as { response?: { data?: { detail?: string } } }).response?.data?.detail || 'Failed to process image',
}); });
ocrFile.value = null; ocrError.value = (err as { response?: { data?: { detail?: string } } }).response?.data?.detail || 'Failed to process image';
} finally { } finally {
ocrLoading.value = false; ocrLoading.value = false;
} }
@ -356,9 +369,10 @@ const addOcrItems = async () => {
for (const item of ocrItems.value) { for (const item of ocrItems.value) {
if (!item.name) continue; if (!item.name) continue;
const response = await api.post<Item>(`/api/v1/lists/${list.value.id}/items`, { const response = await api.post<Item>(
name: item.name, `/api/v1/lists/${list.value.id}/items`,
}); { name: item.name, quantity: 1 }
);
list.value.items.push(response.data); list.value.items.push(response.data);
} }
@ -370,10 +384,10 @@ const addOcrItems = async () => {
showOcrDialog.value = false; showOcrDialog.value = false;
ocrItems.value = []; ocrItems.value = [];
ocrFile.value = null; ocrFile.value = null;
} catch (err: any) { } catch (err: unknown) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
message: err.response?.data?.detail || 'Failed to add items', message: (err as { response?: { data?: { detail?: string } } }).response?.data?.detail || 'Failed to add items',
}); });
} finally { } finally {
addingOcrItems.value = false; addingOcrItems.value = false;
@ -381,7 +395,7 @@ const addOcrItems = async () => {
}; };
onMounted(() => { onMounted(() => {
fetchListDetails().then(() => { void fetchListDetails().then(() => {
startPolling(); startPolling();
}); });
}); });

View File

@ -127,12 +127,10 @@ const fetchLists = async () => {
try { try {
const response = await api.get<List[]>('/lists'); // API returns all accessible lists const response = await api.get<List[]>('/lists'); // API returns all accessible lists
lists.value = response.data; lists.value = response.data;
} catch (err: any) { } catch (err: unknown) {
console.error('Failed to fetch lists:', err); console.error('Failed to fetch lists:', err);
error.value = error.value =
err.response?.data?.detail || err instanceof Error ? err.message : 'An unexpected error occurred while fetching lists.';
err.message ||
'An unexpected error occurred while fetching lists.';
} finally { } finally {
loading.value = false; loading.value = false;
} }