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

View File

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

View File

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

View File

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

View File

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