enh
This commit is contained in:
parent
ca7d8df1c8
commit
128e3b6a58
14 changed files with 326 additions and 505 deletions
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Alert } from 'react-native';
|
import { View, Alert } from 'react-native';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { AppContainer, MyText, tw } from 'common-ui';
|
import { AppContainer, MyText, tw, type ImageUploaderNeoItem } from 'common-ui';
|
||||||
import TagForm from '@/src/components/TagForm';
|
import TagForm from '@/src/components/TagForm';
|
||||||
import { useCreateTag } from '@/src/api-hooks/tag.api';
|
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
|
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
||||||
|
|
||||||
interface TagFormData {
|
interface TagFormData {
|
||||||
tagName: string;
|
tagName: string;
|
||||||
|
|
@ -15,50 +15,47 @@ interface TagFormData {
|
||||||
|
|
||||||
export default function AddTag() {
|
export default function AddTag() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutate: createTag, isPending: isCreating } = useCreateTag();
|
const createTag = trpc.admin.product.createProductTag.useMutation();
|
||||||
const { data: storesData } = trpc.admin.store.getStores.useQuery();
|
const { data: storesData } = trpc.admin.store.getStores.useQuery();
|
||||||
|
const { upload, isUploading } = useUploadToObjectStorage();
|
||||||
|
|
||||||
const handleSubmit = (values: TagFormData, image?: { uri?: string }) => {
|
const handleSubmit = async (values: TagFormData, images: ImageUploaderNeoItem[], _removedExisting: boolean) => {
|
||||||
const formData = new FormData();
|
try {
|
||||||
|
let imageUrl: string | null | undefined;
|
||||||
|
let uploadUrls: string[] = []
|
||||||
|
|
||||||
// Add text fields
|
const newImage = images.find((image) => image.mimeType !== null)
|
||||||
formData.append('tagName', values.tagName);
|
if (newImage) {
|
||||||
if (values.tagDescription) {
|
const response = await fetch(newImage.imgUrl)
|
||||||
formData.append('tagDescription', values.tagDescription);
|
const blob = await response.blob()
|
||||||
}
|
const result = await upload({
|
||||||
formData.append('isDashboardTag', values.isDashboardTag.toString());
|
images: [{ blob, mimeType: newImage.mimeType || 'image/jpeg' }],
|
||||||
|
contextString: 'tags',
|
||||||
// Add related stores
|
})
|
||||||
formData.append('relatedStores', JSON.stringify(values.relatedStores));
|
imageUrl = result.keys[0]
|
||||||
|
uploadUrls = result.presignedUrls
|
||||||
// Add image if uploaded
|
|
||||||
if (image?.uri) {
|
|
||||||
const filename = image.uri.split('/').pop() || 'image.jpg';
|
|
||||||
const match = /\.(\w+)$/.exec(filename);
|
|
||||||
const type = match ? `image/${match[1]}` : 'image/jpeg';
|
|
||||||
|
|
||||||
formData.append('image', {
|
|
||||||
uri: image.uri,
|
|
||||||
name: filename,
|
|
||||||
type,
|
|
||||||
} as any);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createTag(formData, {
|
await createTag.mutateAsync({
|
||||||
onSuccess: (data) => {
|
tagName: values.tagName,
|
||||||
|
tagDescription: values.tagDescription || undefined,
|
||||||
|
imageUrl,
|
||||||
|
isDashboardTag: values.isDashboardTag,
|
||||||
|
relatedStores: values.relatedStores,
|
||||||
|
uploadUrls,
|
||||||
|
})
|
||||||
|
|
||||||
Alert.alert('Success', 'Tag created successfully', [
|
Alert.alert('Success', 'Tag created successfully', [
|
||||||
{
|
{
|
||||||
text: 'OK',
|
text: 'OK',
|
||||||
onPress: () => router.back(),
|
onPress: () => router.back(),
|
||||||
},
|
},
|
||||||
]);
|
])
|
||||||
},
|
} catch (error: any) {
|
||||||
onError: (error: any) => {
|
const errorMessage = error.message || 'Failed to create tag'
|
||||||
const errorMessage = error.message || 'Failed to create tag';
|
Alert.alert('Error', errorMessage)
|
||||||
Alert.alert('Error', errorMessage);
|
}
|
||||||
},
|
}
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialValues: TagFormData = {
|
const initialValues: TagFormData = {
|
||||||
tagName: '',
|
tagName: '',
|
||||||
|
|
@ -76,8 +73,8 @@ export default function AddTag() {
|
||||||
mode="create"
|
mode="create"
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
isLoading={isCreating}
|
isLoading={createTag.isPending || isUploading}
|
||||||
stores={storesData?.stores.map(store => ({ id: store.id, name: store.name })) || []}
|
stores={storesData?.stores.map((store: { id: number; name: string }) => ({ id: store.id, name: store.name })) || []}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Alert } from 'react-native';
|
import { View, Alert } from 'react-native';
|
||||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||||
import { AppContainer, MyText, tw } from 'common-ui';
|
import { AppContainer, MyText, tw, type ImageUploaderNeoItem } from 'common-ui';
|
||||||
import TagForm from '@/src/components/TagForm';
|
import TagForm from '@/src/components/TagForm';
|
||||||
import { useGetTag, useUpdateTag } from '@/src/api-hooks/tag.api';
|
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
|
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
||||||
|
|
||||||
interface TagFormData {
|
interface TagFormData {
|
||||||
tagName: string;
|
tagName: string;
|
||||||
|
|
@ -19,53 +19,56 @@ export default function EditTag() {
|
||||||
const { tagId } = useLocalSearchParams<{ tagId: string }>();
|
const { tagId } = useLocalSearchParams<{ tagId: string }>();
|
||||||
const tagIdNum = tagId ? parseInt(tagId) : null;
|
const tagIdNum = tagId ? parseInt(tagId) : null;
|
||||||
|
|
||||||
const { data: tagData, isLoading: isLoadingTag, error: tagError } = useGetTag(tagIdNum!);
|
const { data: tagData, isLoading: isLoadingTag, error: tagError } = trpc.admin.product.getProductTagById.useQuery(
|
||||||
const { mutate: updateTag, isPending: isUpdating } = useUpdateTag();
|
{ id: tagIdNum || 0 },
|
||||||
|
{ enabled: !!tagIdNum }
|
||||||
|
)
|
||||||
|
const updateTag = trpc.admin.product.updateProductTag.useMutation();
|
||||||
const { data: storesData } = trpc.admin.store.getStores.useQuery();
|
const { data: storesData } = trpc.admin.store.getStores.useQuery();
|
||||||
|
const { upload, isUploading } = useUploadToObjectStorage();
|
||||||
|
|
||||||
const handleSubmit = (values: TagFormData, image?: { uri?: string }) => {
|
const handleSubmit = async (values: TagFormData, images: ImageUploaderNeoItem[], removedExisting: boolean) => {
|
||||||
if (!tagIdNum) return;
|
if (!tagIdNum) return;
|
||||||
|
|
||||||
const formData = new FormData();
|
try {
|
||||||
|
let imageUrl: string | null | undefined
|
||||||
|
let uploadUrls: string[] = []
|
||||||
|
|
||||||
// Add text fields
|
const newImage = images.find((image) => image.mimeType !== null)
|
||||||
formData.append('tagName', values.tagName);
|
if (newImage) {
|
||||||
if (values.tagDescription) {
|
const response = await fetch(newImage.imgUrl)
|
||||||
formData.append('tagDescription', values.tagDescription);
|
const blob = await response.blob()
|
||||||
}
|
const result = await upload({
|
||||||
formData.append('isDashboardTag', values.isDashboardTag.toString());
|
images: [{ blob, mimeType: newImage.mimeType || 'image/jpeg' }],
|
||||||
|
contextString: 'tags',
|
||||||
// Add related stores
|
})
|
||||||
formData.append('relatedStores', JSON.stringify(values.relatedStores));
|
imageUrl = result.keys[0]
|
||||||
|
uploadUrls = result.presignedUrls
|
||||||
// Add image if uploaded
|
} else if (removedExisting) {
|
||||||
if (image?.uri) {
|
imageUrl = null
|
||||||
const filename = image.uri.split('/').pop() || 'image.jpg';
|
|
||||||
const match = /\.(\w+)$/.exec(filename);
|
|
||||||
const type = match ? `image/${match[1]}` : 'image/jpeg';
|
|
||||||
|
|
||||||
formData.append('image', {
|
|
||||||
uri: image.uri,
|
|
||||||
name: filename,
|
|
||||||
type,
|
|
||||||
} as any);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTag({ id: tagIdNum, formData }, {
|
await updateTag.mutateAsync({
|
||||||
onSuccess: (data) => {
|
id: tagIdNum,
|
||||||
|
tagName: values.tagName,
|
||||||
|
tagDescription: values.tagDescription || undefined,
|
||||||
|
imageUrl,
|
||||||
|
isDashboardTag: values.isDashboardTag,
|
||||||
|
relatedStores: values.relatedStores,
|
||||||
|
uploadUrls,
|
||||||
|
})
|
||||||
|
|
||||||
Alert.alert('Success', 'Tag updated successfully', [
|
Alert.alert('Success', 'Tag updated successfully', [
|
||||||
{
|
{
|
||||||
text: 'OK',
|
text: 'OK',
|
||||||
onPress: () => router.back(),
|
onPress: () => router.back(),
|
||||||
},
|
},
|
||||||
]);
|
])
|
||||||
},
|
} catch (error: any) {
|
||||||
onError: (error: any) => {
|
const errorMessage = error.message || 'Failed to update tag'
|
||||||
const errorMessage = error.message || 'Failed to update tag';
|
Alert.alert('Error', errorMessage)
|
||||||
Alert.alert('Error', errorMessage);
|
}
|
||||||
},
|
}
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoadingTag) {
|
if (isLoadingTag) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -92,7 +95,7 @@ export default function EditTag() {
|
||||||
tagName: tag.tagName,
|
tagName: tag.tagName,
|
||||||
tagDescription: tag.tagDescription || '',
|
tagDescription: tag.tagDescription || '',
|
||||||
isDashboardTag: tag.isDashboardTag,
|
isDashboardTag: tag.isDashboardTag,
|
||||||
relatedStores: tag.relatedStores || [],
|
relatedStores: Array.isArray(tag.relatedStores) ? tag.relatedStores : [],
|
||||||
existingImageUrl: tag.imageUrl || undefined,
|
existingImageUrl: tag.imageUrl || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -106,8 +109,8 @@ export default function EditTag() {
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
existingImageUrl={tag.imageUrl || undefined}
|
existingImageUrl={tag.imageUrl || undefined}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
isLoading={isUpdating}
|
isLoading={updateTag.isPending || isUploading}
|
||||||
stores={storesData?.stores.map(store => ({ id: store.id, name: store.name })) || []}
|
stores={storesData?.stores.map((store: { id: number; name: string }) => ({ id: store.id, name: store.name })) || []}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,20 @@ import { useRouter } from 'expo-router';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { tw, MyText, useManualRefresh, useMarkDataFetchers, MyFlatList } from 'common-ui';
|
import { tw, MyText, useManualRefresh, useMarkDataFetchers, MyFlatList } from 'common-ui';
|
||||||
import { TagMenu } from '@/src/components/TagMenu';
|
import { TagMenu } from '@/src/components/TagMenu';
|
||||||
import { useGetTags, Tag } from '@/src/api-hooks/tag.api';
|
import { trpc } from '@/src/trpc-client';
|
||||||
|
|
||||||
|
interface TagItemData {
|
||||||
|
id: number;
|
||||||
|
tagName: string;
|
||||||
|
tagDescription: string | null;
|
||||||
|
imageUrl: string | null;
|
||||||
|
isDashboardTag: boolean;
|
||||||
|
relatedStores?: unknown;
|
||||||
|
createdAt: string | Date;
|
||||||
|
}
|
||||||
|
|
||||||
interface TagItemProps {
|
interface TagItemProps {
|
||||||
item: Tag;
|
item: TagItemData;
|
||||||
onDeleteSuccess: () => void;
|
onDeleteSuccess: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,7 +70,7 @@ const TagHeader: React.FC<TagHeaderProps> = ({ onAddNewTag }) => (
|
||||||
|
|
||||||
export default function ProductTags() {
|
export default function ProductTags() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data: tagsData, isLoading, error, refetch } = useGetTags();
|
const { data: tagsData, isLoading, error, refetch } = trpc.admin.product.getProductTags.useQuery();
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
const tags = tagsData?.tags || [];
|
const tags = tagsData?.tags || [];
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { trpc } from '../src/trpc-client';
|
import { trpc } from '../src/trpc-client';
|
||||||
|
|
||||||
type ContextString = 'review' | 'product_info' | 'notification' | 'store' | 'complaint' | 'profile';
|
type ContextString = 'review' | 'product_info' | 'notification' | 'store' | 'complaint' | 'profile' | 'tags';
|
||||||
|
|
||||||
interface UploadInput {
|
interface UploadInput {
|
||||||
blob: Blob;
|
blob: Blob;
|
||||||
|
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import axios from '../../services/axios-admin-ui';
|
|
||||||
|
|
||||||
// Types
|
|
||||||
export interface CreateTagPayload {
|
|
||||||
tagName: string;
|
|
||||||
tagDescription?: string;
|
|
||||||
imageUrl?: string;
|
|
||||||
isDashboardTag: boolean;
|
|
||||||
relatedStores?: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateTagPayload {
|
|
||||||
tagName: string;
|
|
||||||
tagDescription?: string;
|
|
||||||
imageUrl?: string;
|
|
||||||
isDashboardTag: boolean;
|
|
||||||
relatedStores?: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Tag {
|
|
||||||
id: number;
|
|
||||||
tagName: string;
|
|
||||||
tagDescription: string | null;
|
|
||||||
imageUrl: string | null;
|
|
||||||
isDashboardTag: boolean;
|
|
||||||
relatedStores: number[];
|
|
||||||
createdAt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateTagResponse {
|
|
||||||
tag: Tag;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GetTagsResponse {
|
|
||||||
tags: Tag[];
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API functions
|
|
||||||
const createTagApi = async (formData: FormData): Promise<CreateTagResponse> => {
|
|
||||||
const response = await axios.post('/av/product-tags', formData, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTagApi = async ({ id, formData }: { id: number; formData: FormData }): Promise<CreateTagResponse> => {
|
|
||||||
const response = await axios.put(`/av/product-tags/${id}`, formData, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteTagApi = async (id: number): Promise<{ message: string }> => {
|
|
||||||
const response = await axios.delete(`/av/product-tags/${id}`);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTagsApi = async (): Promise<GetTagsResponse> => {
|
|
||||||
const response = await axios.get('/av/product-tags');
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTagApi = async (id: number): Promise<{ tag: Tag }> => {
|
|
||||||
const response = await axios.get(`/av/product-tags/${id}`);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hooks
|
|
||||||
export const useCreateTag = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: createTagApi,
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['tags'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useUpdateTag = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: updateTagApi,
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['tags'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useDeleteTag = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: deleteTagApi,
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['tags'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGetTags = () => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ['tags'],
|
|
||||||
queryFn: getTagsApi,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGetTag = (id: number) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ['tags', id],
|
|
||||||
queryFn: () => getTagApi(id),
|
|
||||||
enabled: !!id,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -5,7 +5,6 @@ import * as Yup from 'yup';
|
||||||
import { MyTextInput, BottomDropdown, MyText, useTheme, DatePicker, tw, useFocusCallback, Checkbox, ImageUploaderNeo, ImageUploaderNeoItem, ImageUploaderNeoPayload } from 'common-ui';
|
import { MyTextInput, BottomDropdown, MyText, useTheme, DatePicker, tw, useFocusCallback, Checkbox, ImageUploaderNeo, ImageUploaderNeoItem, ImageUploaderNeoPayload } from 'common-ui';
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
import { trpc } from '../trpc-client';
|
import { trpc } from '../trpc-client';
|
||||||
import { useGetTags } from '../api-hooks/tag.api';
|
|
||||||
|
|
||||||
interface ProductFormData {
|
interface ProductFormData {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -71,7 +70,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
value: store.id,
|
value: store.id,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const { data: tagsData } = useGetTags();
|
const { data: tagsData } = trpc.admin.product.getProductTags.useQuery();
|
||||||
const tagOptions = tagsData?.tags.map(tag => ({
|
const tagOptions = tagsData?.tags.map(tag => ({
|
||||||
label: tag.tagName,
|
label: tag.tagName,
|
||||||
value: tag.id.toString(),
|
value: tag.id.toString(),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import React, { useState, useEffect, forwardRef, useCallback } from 'react';
|
import React, { useState, useEffect, forwardRef, useCallback } from 'react';
|
||||||
import { View, TouchableOpacity } from 'react-native';
|
import { View, TouchableOpacity } from 'react-native';
|
||||||
import { Image } from 'expo-image';
|
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { MyTextInput, MyText, Checkbox, ImageUploader, tw, useFocusCallback, BottomDropdown } from 'common-ui';
|
import { MyTextInput, MyText, Checkbox, ImageUploaderNeo, tw, useFocusCallback, BottomDropdown, type ImageUploaderNeoItem, type ImageUploaderNeoPayload } from 'common-ui';
|
||||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
|
|
||||||
interface StoreOption {
|
interface StoreOption {
|
||||||
|
|
@ -23,7 +21,7 @@ interface TagFormProps {
|
||||||
mode: 'create' | 'edit';
|
mode: 'create' | 'edit';
|
||||||
initialValues: TagFormData;
|
initialValues: TagFormData;
|
||||||
existingImageUrl?: string;
|
existingImageUrl?: string;
|
||||||
onSubmit: (values: TagFormData, image?: { uri?: string }) => void;
|
onSubmit: (values: TagFormData, images: ImageUploaderNeoItem[], removedExisting: boolean) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
stores?: StoreOption[];
|
stores?: StoreOption[];
|
||||||
}
|
}
|
||||||
|
|
@ -36,22 +34,20 @@ const TagForm = forwardRef<any, TagFormProps>(({
|
||||||
isLoading,
|
isLoading,
|
||||||
stores = [],
|
stores = [],
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const [image, setImage] = useState<{ uri?: string } | null>(null);
|
const [images, setImages] = useState<ImageUploaderNeoItem[]>([])
|
||||||
|
const [removedExisting, setRemovedExisting] = useState(false)
|
||||||
const [isDashboardTagChecked, setIsDashboardTagChecked] = useState<boolean>(Boolean(initialValues.isDashboardTag));
|
const [isDashboardTagChecked, setIsDashboardTagChecked] = useState<boolean>(Boolean(initialValues.isDashboardTag));
|
||||||
|
|
||||||
// Update checkbox when initial values change
|
// Update checkbox when initial values change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsDashboardTagChecked(Boolean(initialValues.isDashboardTag));
|
setIsDashboardTagChecked(Boolean(initialValues.isDashboardTag));
|
||||||
existingImageUrl && setImage({uri:existingImageUrl})
|
if (existingImageUrl) {
|
||||||
}, [initialValues.isDashboardTag]);
|
setImages([{ imgUrl: existingImageUrl, mimeType: null }])
|
||||||
|
} else {
|
||||||
const pickImage = usePickImage({
|
setImages([])
|
||||||
setFile: (files) => {
|
}
|
||||||
|
setRemovedExisting(false)
|
||||||
setImage(files || null)
|
}, [existingImageUrl, initialValues.isDashboardTag]);
|
||||||
},
|
|
||||||
multiple: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const validationSchema = Yup.object().shape({
|
const validationSchema = Yup.object().shape({
|
||||||
|
|
@ -67,14 +63,14 @@ const TagForm = forwardRef<any, TagFormProps>(({
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
validationSchema={validationSchema}
|
validationSchema={validationSchema}
|
||||||
onSubmit={(values) => onSubmit(values, image || undefined)}
|
onSubmit={(values) => onSubmit(values, images, removedExisting)}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
>
|
>
|
||||||
{({ handleChange, handleSubmit, values, setFieldValue, errors, touched, setFieldValue: formikSetFieldValue, resetForm }) => {
|
{({ handleChange, handleSubmit, values, setFieldValue, errors, touched, setFieldValue: formikSetFieldValue, resetForm }) => {
|
||||||
// Clear form when screen comes into focus
|
// Clear form when screen comes into focus
|
||||||
const clearForm = useCallback(() => {
|
const clearForm = useCallback(() => {
|
||||||
setImage(null);
|
setImages([])
|
||||||
|
setRemovedExisting(false)
|
||||||
setIsDashboardTagChecked(false);
|
setIsDashboardTagChecked(false);
|
||||||
resetForm();
|
resetForm();
|
||||||
}, [resetForm]);
|
}, [resetForm]);
|
||||||
|
|
@ -108,10 +104,21 @@ const TagForm = forwardRef<any, TagFormProps>(({
|
||||||
</MyText>
|
</MyText>
|
||||||
|
|
||||||
|
|
||||||
<ImageUploader
|
<ImageUploaderNeo
|
||||||
images={image ? [image] : []}
|
images={images}
|
||||||
onAddImage={pickImage}
|
onImageAdd={(payload: ImageUploaderNeoPayload[]) => {
|
||||||
onRemoveImage={() => setImage(null)}
|
setImages((prev) => [...prev, ...payload.map((img) => ({
|
||||||
|
imgUrl: img.url,
|
||||||
|
mimeType: img.mimeType,
|
||||||
|
}))])
|
||||||
|
}}
|
||||||
|
onImageRemove={(payload) => {
|
||||||
|
if (payload.mimeType === null) {
|
||||||
|
setRemovedExisting(true)
|
||||||
|
}
|
||||||
|
setImages((prev) => prev.filter((item) => item.imgUrl !== payload.url))
|
||||||
|
}}
|
||||||
|
allowMultiple={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { View, TouchableOpacity, Alert } from 'react-native';
|
||||||
import { Entypo } from '@expo/vector-icons';
|
import { Entypo } from '@expo/vector-icons';
|
||||||
import { MyText, tw, BottomDialog } from 'common-ui';
|
import { MyText, tw, BottomDialog } from 'common-ui';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { useDeleteTag } from '../api-hooks/tag.api';
|
import { trpc } from '@/src/trpc-client';
|
||||||
|
|
||||||
export interface TagMenuProps {
|
export interface TagMenuProps {
|
||||||
tagId: number;
|
tagId: number;
|
||||||
|
|
@ -22,7 +22,7 @@ export const TagMenu: React.FC<TagMenuProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutate: deleteTag, isPending: isDeleting } = useDeleteTag();
|
const deleteTag = trpc.admin.product.deleteProductTag.useMutation();
|
||||||
|
|
||||||
const handleOpenMenu = () => {
|
const handleOpenMenu = () => {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
|
|
@ -54,7 +54,7 @@ export const TagMenu: React.FC<TagMenuProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const performDelete = () => {
|
const performDelete = () => {
|
||||||
deleteTag(tagId, {
|
deleteTag.mutate({ id: tagId }, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
Alert.alert('Success', 'Tag deleted successfully');
|
Alert.alert('Success', 'Tag deleted successfully');
|
||||||
onDeleteSuccess?.();
|
onDeleteSuccess?.();
|
||||||
|
|
@ -63,7 +63,7 @@ export const TagMenu: React.FC<TagMenuProps> = ({
|
||||||
const errorMessage = error.message || 'Failed to delete tag';
|
const errorMessage = error.message || 'Failed to delete tag';
|
||||||
Alert.alert('Error', errorMessage);
|
Alert.alert('Error', errorMessage);
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { authenticateStaff } from "@/src/middleware/staff-auth";
|
import { authenticateStaff } from "@/src/middleware/staff-auth";
|
||||||
import productRouter from "@/src/apis/admin-apis/apis/product.router"
|
import productRouter from "@/src/apis/admin-apis/apis/product.router"
|
||||||
import tagRouter from "@/src/apis/admin-apis/apis/tag.router"
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|
@ -11,9 +10,6 @@ router.use(authenticateStaff);
|
||||||
// Product routes
|
// Product routes
|
||||||
router.use("/products", productRouter);
|
router.use("/products", productRouter);
|
||||||
|
|
||||||
// Tag routes
|
|
||||||
router.use("/product-tags", tagRouter);
|
|
||||||
|
|
||||||
const avRouter = router;
|
const avRouter = router;
|
||||||
|
|
||||||
export default avRouter;
|
export default avRouter;
|
||||||
|
|
@ -1,217 +0,0 @@
|
||||||
import { Request, Response } from "express";
|
|
||||||
import {
|
|
||||||
checkProductTagExistsByName,
|
|
||||||
createProductTag,
|
|
||||||
deleteProductTag,
|
|
||||||
getAllProductTagInfos,
|
|
||||||
getProductTagInfoById,
|
|
||||||
updateProductTag,
|
|
||||||
} from '@/src/dbService'
|
|
||||||
import { ApiError } from "@/src/lib/api-error";
|
|
||||||
import { imageUploadS3, generateSignedUrlFromS3Url } from "@/src/lib/s3-client";
|
|
||||||
import { deleteS3Image } from "@/src/lib/delete-image";
|
|
||||||
import { scheduleStoreInitialization } from '@/src/stores/store-initializer';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new product tag
|
|
||||||
*/
|
|
||||||
export const createTag = async (req: Request, res: Response) => {
|
|
||||||
const { tagName, tagDescription, isDashboardTag, relatedStores } = req.body;
|
|
||||||
|
|
||||||
if (!tagName) {
|
|
||||||
throw new ApiError("Tag name is required", 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate tag name
|
|
||||||
const existingTag = await checkProductTagExistsByName(tagName.trim())
|
|
||||||
|
|
||||||
if (existingTag) {
|
|
||||||
throw new ApiError("A tag with this name already exists", 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
let imageUrl: string | null = null;
|
|
||||||
|
|
||||||
// Handle image upload if file is provided
|
|
||||||
if (req.file) {
|
|
||||||
const key = `tags/${Date.now()}-${req.file.originalname}`;
|
|
||||||
imageUrl = await imageUploadS3(req.file.buffer, req.file.mimetype, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse relatedStores if it's a string (from FormData)
|
|
||||||
let parsedRelatedStores: number[] = [];
|
|
||||||
if (relatedStores) {
|
|
||||||
try {
|
|
||||||
parsedRelatedStores = typeof relatedStores === 'string'
|
|
||||||
? JSON.parse(relatedStores)
|
|
||||||
: relatedStores;
|
|
||||||
} catch (e) {
|
|
||||||
parsedRelatedStores = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdTag = await createProductTag({
|
|
||||||
tagName: tagName.trim(),
|
|
||||||
tagDescription,
|
|
||||||
imageUrl,
|
|
||||||
isDashboardTag: isDashboardTag || false,
|
|
||||||
relatedStores: parsedRelatedStores,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { products, ...newTag } = createdTag
|
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes in cache
|
|
||||||
scheduleStoreInitialization()
|
|
||||||
|
|
||||||
// Send response first
|
|
||||||
res.status(201).json({
|
|
||||||
tag: newTag,
|
|
||||||
message: "Tag created successfully",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all product tags
|
|
||||||
*/
|
|
||||||
export const getAllTags = async (req: Request, res: Response) => {
|
|
||||||
const tags = await getAllProductTagInfos()
|
|
||||||
|
|
||||||
// Generate signed URLs for tag images
|
|
||||||
const tagsWithSignedUrls = await Promise.all(
|
|
||||||
tags.map(async (tag) => ({
|
|
||||||
...tag,
|
|
||||||
imageUrl: tag.imageUrl ? await generateSignedUrlFromS3Url(tag.imageUrl) : null,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
tags: tagsWithSignedUrls,
|
|
||||||
message: "Tags retrieved successfully",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a single product tag by ID
|
|
||||||
*/
|
|
||||||
export const getTagById = async (req: Request, res: Response) => {
|
|
||||||
const id = req.params.id as string
|
|
||||||
|
|
||||||
const tag = await getProductTagInfoById(parseInt(id))
|
|
||||||
|
|
||||||
if (!tag) {
|
|
||||||
throw new ApiError("Tag not found", 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate signed URL for tag image
|
|
||||||
const tagWithSignedUrl = {
|
|
||||||
...tag,
|
|
||||||
imageUrl: tag.imageUrl ? await generateSignedUrlFromS3Url(tag.imageUrl) : null,
|
|
||||||
};
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
tag: tagWithSignedUrl,
|
|
||||||
message: "Tag retrieved successfully",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a product tag
|
|
||||||
*/
|
|
||||||
export const updateTag = async (req: Request, res: Response) => {
|
|
||||||
const id = req.params.id as string
|
|
||||||
const { tagName, tagDescription, isDashboardTag, relatedStores } = req.body;
|
|
||||||
|
|
||||||
// Get the current tag to check for existing image
|
|
||||||
const currentTag = await getProductTagInfoById(parseInt(id))
|
|
||||||
|
|
||||||
if (!currentTag) {
|
|
||||||
throw new ApiError("Tag not found", 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
let imageUrl = currentTag.imageUrl;
|
|
||||||
|
|
||||||
// Handle image upload if new file is provided
|
|
||||||
if (req.file) {
|
|
||||||
// Delete old image if it exists
|
|
||||||
if (currentTag.imageUrl) {
|
|
||||||
try {
|
|
||||||
await deleteS3Image(currentTag.imageUrl);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to delete old image:", error);
|
|
||||||
// Continue with update even if delete fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Upload new image
|
|
||||||
const key = `tags/${Date.now()}-${req.file.originalname}`;
|
|
||||||
console.log('file', key)
|
|
||||||
imageUrl = await imageUploadS3(req.file.buffer, req.file.mimetype, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse relatedStores if it's a string (from FormData)
|
|
||||||
let parsedRelatedStores: number[] | undefined;
|
|
||||||
if (relatedStores !== undefined) {
|
|
||||||
try {
|
|
||||||
parsedRelatedStores = typeof relatedStores === 'string'
|
|
||||||
? JSON.parse(relatedStores)
|
|
||||||
: relatedStores;
|
|
||||||
} catch (e) {
|
|
||||||
parsedRelatedStores = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedTag = await updateProductTag(parseInt(id), {
|
|
||||||
tagName: tagName?.trim(),
|
|
||||||
tagDescription,
|
|
||||||
imageUrl,
|
|
||||||
isDashboardTag,
|
|
||||||
relatedStores: parsedRelatedStores,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { products, ...updatedTagInfo } = updatedTag
|
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes in cache
|
|
||||||
scheduleStoreInitialization()
|
|
||||||
|
|
||||||
// Send response first
|
|
||||||
res.status(200).json({
|
|
||||||
tag: updatedTagInfo,
|
|
||||||
message: "Tag updated successfully",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a product tag
|
|
||||||
*/
|
|
||||||
export const deleteTag = async (req: Request, res: Response) => {
|
|
||||||
const id = req.params.id as string
|
|
||||||
|
|
||||||
// Check if tag exists
|
|
||||||
const tag = await getProductTagInfoById(parseInt(id))
|
|
||||||
|
|
||||||
if (!tag) {
|
|
||||||
throw new ApiError("Tag not found", 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete image from S3 if it exists
|
|
||||||
if (tag.imageUrl) {
|
|
||||||
try {
|
|
||||||
await deleteS3Image(tag.imageUrl);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to delete image from S3:", error);
|
|
||||||
// Continue with deletion even if image delete fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: This will fail if tag is still assigned to products due to foreign key constraint
|
|
||||||
await deleteProductTag(parseInt(id))
|
|
||||||
|
|
||||||
// Reinitialize stores to reflect changes in cache
|
|
||||||
scheduleStoreInitialization()
|
|
||||||
|
|
||||||
// Send response first
|
|
||||||
res.status(200).json({
|
|
||||||
message: "Tag deleted successfully",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { Router } from "express";
|
|
||||||
import { createTag, getAllTags, getTagById, updateTag, deleteTag } from "@/src/apis/admin-apis/apis/product-tags.controller"
|
|
||||||
import uploadHandler from '@/src/lib/upload-handler';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
// Tag routes
|
|
||||||
router.post("/", uploadHandler.single('image'), createTag);
|
|
||||||
router.get("/", getAllTags);
|
|
||||||
router.get("/:id", getTagById);
|
|
||||||
router.put("/:id", uploadHandler.single('image'), updateTag);
|
|
||||||
router.delete("/:id", deleteTag);
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { ApiError } from '@/src/lib/api-error'
|
import { ApiError } from '@/src/lib/api-error'
|
||||||
import { generateSignedUrlsFromS3Urls, claimUploadUrl, extractKeyFromPresignedUrl, deleteImageUtil } from '@/src/lib/s3-client'
|
import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url, claimUploadUrl, extractKeyFromPresignedUrl, deleteImageUtil } from '@/src/lib/s3-client'
|
||||||
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
|
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
|
||||||
import {
|
import {
|
||||||
getAllProducts as getAllProductsInDb,
|
getAllProducts as getAllProductsInDb,
|
||||||
|
|
@ -26,6 +26,12 @@ import {
|
||||||
getProductImagesById,
|
getProductImagesById,
|
||||||
updateProduct as updateProductInDb,
|
updateProduct as updateProductInDb,
|
||||||
updateProductDeals,
|
updateProductDeals,
|
||||||
|
checkProductTagExistsByName,
|
||||||
|
createProductTag as createProductTagInDb,
|
||||||
|
updateProductTag as updateProductTagInDb,
|
||||||
|
deleteProductTag as deleteProductTagInDb,
|
||||||
|
getAllProductTagInfos as getAllProductTagInfosInDb,
|
||||||
|
getProductTagInfoById as getProductTagInfoByIdInDb,
|
||||||
} from '@/src/dbService'
|
} from '@/src/dbService'
|
||||||
import type {
|
import type {
|
||||||
AdminProduct,
|
AdminProduct,
|
||||||
|
|
@ -894,4 +900,153 @@ export const productRouter = router({
|
||||||
updatedCount: result.updatedCount,
|
updatedCount: result.updatedCount,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
getProductTags: protectedProcedure
|
||||||
|
.query(async (): Promise<{ tags: Array<{ id: number; tagName: string; tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; relatedStores: unknown; createdAt: Date }>; message: string }> => {
|
||||||
|
const tags = await getAllProductTagInfosInDb()
|
||||||
|
|
||||||
|
const tagsWithSignedUrls = await Promise.all(
|
||||||
|
tags.map(async (tag) => ({
|
||||||
|
...tag,
|
||||||
|
imageUrl: tag.imageUrl ? await generateSignedUrlFromS3Url(tag.imageUrl) : null,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
tags: tagsWithSignedUrls,
|
||||||
|
message: 'Tags retrieved successfully',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
getProductTagById: protectedProcedure
|
||||||
|
.input(z.object({ id: z.number() }))
|
||||||
|
.query(async ({ input }): Promise<{ tag: { id: number; tagName: string; tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; relatedStores: unknown; createdAt: Date }; message: string }> => {
|
||||||
|
const tag = await getProductTagInfoByIdInDb(input.id)
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
throw new ApiError('Tag not found', 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagWithSignedUrl = {
|
||||||
|
...tag,
|
||||||
|
imageUrl: tag.imageUrl ? await generateSignedUrlFromS3Url(tag.imageUrl) : null,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tag: tagWithSignedUrl,
|
||||||
|
message: 'Tag retrieved successfully',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
createProductTag: protectedProcedure
|
||||||
|
.input(z.object({
|
||||||
|
tagName: z.string().min(1, 'Tag name is required'),
|
||||||
|
tagDescription: z.string().optional(),
|
||||||
|
imageUrl: z.string().optional().nullable(),
|
||||||
|
isDashboardTag: z.boolean().optional().default(false),
|
||||||
|
relatedStores: z.array(z.number()).optional().default([]),
|
||||||
|
uploadUrls: z.array(z.string()).optional().default([]),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input }): Promise<{ tag: { id: number; tagName: string; tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; relatedStores: unknown; createdAt: Date }; message: string }> => {
|
||||||
|
const { tagName, tagDescription, imageUrl, isDashboardTag, relatedStores, uploadUrls } = input
|
||||||
|
|
||||||
|
const existingTag = await checkProductTagExistsByName(tagName.trim())
|
||||||
|
if (existingTag) {
|
||||||
|
throw new ApiError('A tag with this name already exists', 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdTag = await createProductTagInDb({
|
||||||
|
tagName: tagName.trim(),
|
||||||
|
tagDescription,
|
||||||
|
imageUrl: imageUrl ?? null,
|
||||||
|
isDashboardTag,
|
||||||
|
relatedStores,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (uploadUrls.length > 0) {
|
||||||
|
await Promise.all(uploadUrls.map((url) => claimUploadUrl(url)))
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleStoreInitialization()
|
||||||
|
|
||||||
|
const { products, ...createdTagInfo } = createdTag
|
||||||
|
|
||||||
|
return {
|
||||||
|
tag: {
|
||||||
|
...createdTagInfo,
|
||||||
|
imageUrl: createdTagInfo.imageUrl ? await generateSignedUrlFromS3Url(createdTagInfo.imageUrl) : null,
|
||||||
|
},
|
||||||
|
message: 'Tag created successfully',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateProductTag: protectedProcedure
|
||||||
|
.input(z.object({
|
||||||
|
id: z.number(),
|
||||||
|
tagName: z.string().min(1).optional(),
|
||||||
|
tagDescription: z.string().optional().nullable(),
|
||||||
|
imageUrl: z.string().optional().nullable(),
|
||||||
|
isDashboardTag: z.boolean().optional(),
|
||||||
|
relatedStores: z.array(z.number()).optional(),
|
||||||
|
uploadUrls: z.array(z.string()).optional().default([]),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input }): Promise<{ tag: { id: number; tagName: string; tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; relatedStores: unknown; createdAt: Date }; message: string }> => {
|
||||||
|
const { id, tagName, tagDescription, imageUrl, isDashboardTag, relatedStores, uploadUrls } = input
|
||||||
|
|
||||||
|
const currentTag = await getProductTagInfoByIdInDb(id)
|
||||||
|
|
||||||
|
if (!currentTag) {
|
||||||
|
throw new ApiError('Tag not found', 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageUrl !== undefined && imageUrl !== currentTag.imageUrl) {
|
||||||
|
if (currentTag.imageUrl) {
|
||||||
|
await deleteImageUtil({ keys: [currentTag.imageUrl] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedTag = await updateProductTagInDb(id, {
|
||||||
|
tagName: tagName?.trim(),
|
||||||
|
tagDescription: tagDescription ?? undefined,
|
||||||
|
imageUrl: imageUrl ?? undefined,
|
||||||
|
isDashboardTag,
|
||||||
|
relatedStores,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (uploadUrls.length > 0) {
|
||||||
|
await Promise.all(uploadUrls.map((url) => claimUploadUrl(url)))
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleStoreInitialization()
|
||||||
|
|
||||||
|
const { products, ...updatedTagInfo } = updatedTag
|
||||||
|
|
||||||
|
return {
|
||||||
|
tag: {
|
||||||
|
...updatedTagInfo,
|
||||||
|
imageUrl: updatedTagInfo.imageUrl ? await generateSignedUrlFromS3Url(updatedTagInfo.imageUrl) : null,
|
||||||
|
},
|
||||||
|
message: 'Tag updated successfully',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteProductTag: protectedProcedure
|
||||||
|
.input(z.object({ id: z.number() }))
|
||||||
|
.mutation(async ({ input }): Promise<{ message: string }> => {
|
||||||
|
const tag = await getProductTagInfoByIdInDb(input.id)
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
throw new ApiError('Tag not found', 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag.imageUrl) {
|
||||||
|
await deleteImageUtil({ keys: [tag.imageUrl] })
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteProductTagInDb(input.id)
|
||||||
|
|
||||||
|
scheduleStoreInitialization()
|
||||||
|
|
||||||
|
return { message: 'Tag deleted successfully' }
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export const commonApiRouter = router({
|
||||||
|
|
||||||
generateUploadUrls: protectedProcedure
|
generateUploadUrls: protectedProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
contextString: z.enum(['review', 'review_response', 'product_info', 'notification', 'store', 'complaint', 'profile']),
|
contextString: z.enum(['review', 'review_response', 'product_info', 'notification', 'store', 'complaint', 'profile', 'tags']),
|
||||||
mimeTypes: z.array(z.string()),
|
mimeTypes: z.array(z.string()),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => {
|
.mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => {
|
||||||
|
|
@ -106,6 +106,8 @@ export const commonApiRouter = router({
|
||||||
folder = 'complaint-images';
|
folder = 'complaint-images';
|
||||||
} else if (contextString === 'profile') {
|
} else if (contextString === 'profile') {
|
||||||
folder = 'profile-images';
|
folder = 'profile-images';
|
||||||
|
} else if (contextString === 'tags') {
|
||||||
|
folder = 'tags';
|
||||||
} else {
|
} else {
|
||||||
folder = '';
|
folder = '';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { ApiError } from '@/src/lib/api-error';
|
||||||
export const fileUploadRouter = router({
|
export const fileUploadRouter = router({
|
||||||
generateUploadUrls: protectedProcedure
|
generateUploadUrls: protectedProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
contextString: z.enum(['review', 'product_info', 'notification', 'complaint', 'profile']),
|
contextString: z.enum(['review', 'product_info', 'notification', 'complaint', 'profile', 'tags']),
|
||||||
mimeTypes: z.array(z.string()),
|
mimeTypes: z.array(z.string()),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => {
|
.mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => {
|
||||||
|
|
@ -32,6 +32,8 @@ export const fileUploadRouter = router({
|
||||||
folder = 'complaint-images'
|
folder = 'complaint-images'
|
||||||
} else if (contextString === 'profile') {
|
} else if (contextString === 'profile') {
|
||||||
folder = 'profile-images'
|
folder = 'profile-images'
|
||||||
|
} else if (contextString === 'tags') {
|
||||||
|
folder = 'tags'
|
||||||
} else {
|
} else {
|
||||||
folder = '';
|
folder = '';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue