diff --git a/apps/admin-ui/app/(drawer)/products/add.tsx b/apps/admin-ui/app/(drawer)/products/add.tsx
index e49930c..1a5578f 100644
--- a/apps/admin-ui/app/(drawer)/products/add.tsx
+++ b/apps/admin-ui/app/(drawer)/products/add.tsx
@@ -2,53 +2,28 @@ import React from 'react';
import { Alert } from 'react-native';
import { AppContainer } from 'common-ui';
import ProductForm from '@/src/components/ProductForm';
-import { useCreateProduct, CreateProductPayload } from '@/src/api-hooks/product.api';
+import { trpc } from '@/src/trpc-client';
export default function AddProduct() {
- const { mutate: createProduct, isPending: isCreating } = useCreateProduct();
+ const createProduct = trpc.admin.product.createProduct.useMutation();
- const handleSubmit = (values: any, images?: { uri?: string, mimeType?: string }[]) => {
- const payload: CreateProductPayload = {
- name: values.name,
- shortDescription: values.shortDescription,
- longDescription: values.longDescription,
- unitId: parseInt(values.unitId),
- storeId: parseInt(values.storeId),
- price: parseFloat(values.price),
- marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
- incrementStep: 1,
- productQuantity: values.productQuantity || 1,
- };
-
- const formData = new FormData();
- Object.entries(payload).forEach(([key, value]) => {
- if (value !== undefined && value !== null) {
- formData.append(key, value as string);
- }
- });
-
- // Append tag IDs
- if (values.tagIds && values.tagIds.length > 0) {
- values.tagIds.forEach((tagId: number) => {
- formData.append('tagIds', tagId.toString());
- });
- }
-
- // Append images
- if (images) {
- images.forEach((image, index) => {
- if (image.uri) {
- formData.append('images', {
- uri: image.uri,
- name: `image-${index}.jpg`,
- // type: 'image/jpeg',
- type: image.mimeType as any,
- } as any);
- }
- });
- }
-
- createProduct(formData, {
+ const handleSubmit = (values: any, imageKeys?: string[]) => {
+ createProduct.mutate({
+ name: values.name,
+ shortDescription: values.shortDescription,
+ longDescription: values.longDescription,
+ unitId: parseInt(values.unitId),
+ storeId: parseInt(values.storeId),
+ price: parseFloat(values.price),
+ marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
+ incrementStep: 1,
+ productQuantity: values.productQuantity || 1,
+ isSuspended: values.isSuspended || false,
+ isFlashAvailable: values.isFlashAvailable || false,
+ flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : undefined,
+ tagIds: values.tagIds || [],
+ imageKeys: imageKeys || [],
+ }, {
onSuccess: (data) => {
Alert.alert('Success', 'Product created successfully!');
// Reset form or navigate
@@ -73,7 +48,7 @@ export default function AddProduct() {
isFlashAvailable: false,
flashPrice: '',
productQuantity: 1,
- };
+ };
return (
@@ -81,9 +56,9 @@ export default function AddProduct() {
mode="create"
initialValues={initialValues}
onSubmit={handleSubmit}
- isLoading={isCreating}
+ isLoading={createProduct.isPending}
existingImages={[]}
/>
);
-}
\ No newline at end of file
+}
diff --git a/apps/admin-ui/app/(drawer)/products/edit.tsx b/apps/admin-ui/app/(drawer)/products/edit.tsx
index e0bea74..e4cbe7a 100644
--- a/apps/admin-ui/app/(drawer)/products/edit.tsx
+++ b/apps/admin-ui/app/(drawer)/products/edit.tsx
@@ -3,7 +3,6 @@ import { View, Text, Alert } from 'react-native';
import { useLocalSearchParams } from 'expo-router';
import { AppContainer, useManualRefresh, MyText, tw } from 'common-ui';
import ProductForm, { ProductFormRef } from '@/src/components/ProductForm';
-import { useUpdateProduct } from '@/src/api-hooks/product.api';
import { trpc } from '@/src/trpc-client';
export default function EditProduct() {
@@ -11,85 +10,52 @@ export default function EditProduct() {
const productId = Number(id);
const productFormRef = useRef(null);
- // const { data: product, isLoading: isFetching, refetch } = useGetProduct(productId);
const { data: product, isLoading: isFetching, refetch } = trpc.admin.product.getProductById.useQuery(
{ id: productId },
{ enabled: !!productId }
);
- //
- const { mutate: updateProduct, isPending: isUpdating } = useUpdateProduct();
+
+ const updateProduct = trpc.admin.product.updateProduct.useMutation();
useManualRefresh(() => refetch());
- const handleSubmit = (values: any, newImages?: { uri?: string }[], imagesToDelete?: string[]) => {
- const payload = {
- name: values.name,
- shortDescription: values.shortDescription,
- longDescription: values.longDescription,
- unitId: parseInt(values.unitId),
- storeId: parseInt(values.storeId),
- price: parseFloat(values.price),
- marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
- incrementStep: 1,
- productQuantity: values.productQuantity || 1,
- deals: values.deals?.filter((deal: any) =>
+ const handleSubmit = (values: any, newImageKeys?: string[], imagesToDelete?: string[]) => {
+ updateProduct.mutate({
+ id: productId,
+ name: values.name,
+ shortDescription: values.shortDescription,
+ longDescription: values.longDescription,
+ unitId: parseInt(values.unitId),
+ storeId: parseInt(values.storeId),
+ price: parseFloat(values.price),
+ marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
+ incrementStep: 1,
+ productQuantity: values.productQuantity || 1,
+ isSuspended: values.isSuspended,
+ isFlashAvailable: values.isFlashAvailable,
+ flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : undefined,
+ deals: values.deals?.filter((deal: any) =>
deal.quantity && deal.price && deal.validTill
).map((deal: any) => ({
quantity: parseInt(deal.quantity),
price: parseFloat(deal.price),
validTill: deal.validTill instanceof Date
? deal.validTill.toISOString().split('T')[0]
- : deal.validTill, // Convert Date to YYYY-MM-DD string
+ : deal.validTill,
})),
tagIds: values.tagIds,
- };
-
-
- const formData = new FormData();
- Object.entries(payload).forEach(([key, value]) => {
- if (key === 'deals' && Array.isArray(value)) {
- formData.append(key, JSON.stringify(value));
- } else if (key === 'tagIds' && Array.isArray(value)) {
- value.forEach(tagId => {
- formData.append('tagIds', tagId.toString());
- });
- } else if (value !== undefined && value !== null) {
- formData.append(key, value as string);
- }
+ newImageKeys: newImageKeys || [],
+ imagesToDelete: imagesToDelete || [],
+ }, {
+ onSuccess: (data) => {
+ Alert.alert('Success', 'Product updated successfully!');
+ // Clear newly added images after successful update
+ productFormRef.current?.clearImages();
+ },
+ onError: (error: any) => {
+ Alert.alert('Error', error.message || 'Failed to update product');
+ },
});
-
- // Add new images
- if (newImages && newImages.length > 0) {
- newImages.forEach((image, index) => {
- if (image.uri) {
- const fileName = image.uri.split('/').pop() || `image_${index}.jpg`;
- formData.append('images', {
- uri: image.uri,
- name: fileName,
- type: 'image/jpeg',
- } as any);
- }
- });
- }
-
- // Add images to delete
- if (imagesToDelete && imagesToDelete.length > 0) {
- formData.append('imagesToDelete', JSON.stringify(imagesToDelete));
- }
-
- updateProduct(
- { id: productId, formData },
- {
- onSuccess: (data) => {
- Alert.alert('Success', 'Product updated successfully!');
- // Clear newly added images after successful update
- productFormRef.current?.clearImages();
- },
- onError: (error: any) => {
- Alert.alert('Error', error.message || 'Failed to update product');
- },
- }
- );
};
if (isFetching) {
@@ -125,7 +91,7 @@ export default function EditProduct() {
deals: productData.deals?.map(deal => ({
quantity: deal.quantity,
price: deal.price,
- validTill: deal.validTill ? new Date(deal.validTill) : null, // Convert to Date object
+ validTill: deal.validTill ? new Date(deal.validTill) : null,
})) || [{ quantity: '', price: '', validTill: null }],
tagIds: productData.tags?.map((tag: any) => tag.id) || [],
isSuspended: productData.isSuspended || false,
@@ -141,9 +107,9 @@ export default function EditProduct() {
mode="edit"
initialValues={initialValues}
onSubmit={handleSubmit}
- isLoading={isUpdating}
+ isLoading={updateProduct.isPending}
existingImages={productData.images || []}
/>
);
-}
\ No newline at end of file
+}
diff --git a/apps/admin-ui/hooks/useUploadToObjectStorage.ts b/apps/admin-ui/hooks/useUploadToObjectStorage.ts
index e88250c..abd7d2c 100644
--- a/apps/admin-ui/hooks/useUploadToObjectStorage.ts
+++ b/apps/admin-ui/hooks/useUploadToObjectStorage.ts
@@ -112,6 +112,7 @@ export function useUploadToObjectStorage() {
function extractKeyFromPresignedUrl(url: string): string {
const u = new URL(url);
- const rawKey = u.pathname.replace(/^\/+/, '');
+ let rawKey = u.pathname.replace(/^\/+/, '');
+ rawKey = rawKey.split('/').slice(1).join('/'); // make meatfarmer/product-images/asdf as product-images/asdf
return decodeURIComponent(rawKey);
}
diff --git a/apps/admin-ui/src/api-hooks/product.api.ts b/apps/admin-ui/src/api-hooks/product.api.ts
deleted file mode 100644
index bdb42ed..0000000
--- a/apps/admin-ui/src/api-hooks/product.api.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { useMutation, useQueryClient } from "@tanstack/react-query";
-import axios from '../../services/axios-admin-ui';
-
-// Types
-export interface CreateProductPayload {
- name: string;
- shortDescription?: string;
- longDescription?: string;
- unitId: number;
- storeId: number;
- price: number;
- marketPrice?: number;
- incrementStep?: number;
- productQuantity?: number;
- isOutOfStock?: boolean;
- deals?: {
- quantity: number;
- price: number;
- validTill: string;
- }[];
-}
-
-export interface UpdateProductPayload {
- name: string;
- shortDescription?: string;
- longDescription?: string;
- unitId: number;
- storeId: number;
- price: number;
- marketPrice?: number;
- incrementStep?: number;
- productQuantity?: number;
- isOutOfStock?: boolean;
- deals?: {
- quantity: number;
- price: number;
- validTill: string;
- }[];
-}
-
-export interface Product {
- id: number;
- name: string;
- shortDescription?: string | null;
- longDescription?: string;
- unitId: number;
- storeId: number;
- price: number;
- marketPrice?: number;
- productQuantity?: number;
- isOutOfStock?: boolean;
- images?: string[];
- createdAt: string;
- unit?: {
- id: number;
- shortNotation: string;
- fullName: string;
- };
- deals?: {
- id: number;
- quantity: string;
- price: string;
- validTill: string;
- }[];
-}
-
-export interface CreateProductResponse {
- product: Product;
- deals?: any[];
- message: string;
-}
-
-// API functions
-const createProductApi = async (formData: FormData): Promise => {
- const response = await axios.post('/av/products', formData, {
- headers: {
- 'Content-Type': 'multipart/form-data',
- },
- });
- return response.data;
-};
-
-const updateProductApi = async ({ id, formData }: { id: number; formData: FormData }): Promise => {
- const response = await axios.put(`/av/products/${id}`, formData, {
- headers: {
- 'Content-Type': 'multipart/form-data',
- },
- });
- return response.data;
-};
-
-// Hooks
-export const useCreateProduct = () => {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: createProductApi,
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['products'] });
- },
- });
-};
-
-export const useUpdateProduct = () => {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: updateProductApi,
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['products'] });
- },
- });
-};
diff --git a/apps/admin-ui/src/components/ProductForm.tsx b/apps/admin-ui/src/components/ProductForm.tsx
index 1502214..92baf32 100644
--- a/apps/admin-ui/src/components/ProductForm.tsx
+++ b/apps/admin-ui/src/components/ProductForm.tsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle } from 'react';
-import { View, TouchableOpacity } from 'react-native';
+import { View, TouchableOpacity, Alert } from 'react-native';
import { Image } from 'expo-image';
import { Formik, FieldArray } from 'formik';
import * as Yup from 'yup';
@@ -8,6 +8,7 @@ import usePickImage from 'common-ui/src/components/use-pick-image';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { trpc } from '../trpc-client';
import { useGetTags } from '../api-hooks/tag.api';
+import { useUploadToObjectStorage } from '../../hooks/useUploadToObjectStorage';
interface ProductFormData {
name: string;
@@ -38,7 +39,7 @@ export interface ProductFormRef {
interface ProductFormProps {
mode: 'create' | 'edit';
initialValues: ProductFormData;
- onSubmit: (values: ProductFormData, images?: { uri?: string }[], imagesToDelete?: string[]) => void;
+ onSubmit: (values: ProductFormData, imageKeys?: string[], imagesToDelete?: string[]) => void;
isLoading: boolean;
existingImages?: string[];
}
@@ -60,8 +61,9 @@ const ProductForm = forwardRef(({
existingImages = []
}, ref) => {
const { theme } = useTheme();
- const [images, setImages] = useState<{ uri?: string }[]>([]);
+ const [newImages, setNewImages] = useState<{ blob: Blob; mimeType: string; uri: string }[]>([]);
const [existingImagesState, setExistingImagesState] = useState(existingImages);
+ const { upload, isUploading } = useUploadToObjectStorage();
const { data: storesData } = trpc.common.getStoresSummary.useQuery();
const storeOptions = storesData?.stores.map(store => ({
@@ -83,23 +85,62 @@ const ProductForm = forwardRef(({
}, [existingImages]);
const pickImage = usePickImage({
- setFile: (files) => setImages(prev => [...prev, ...files]),
+ setFile: async (assets: any) => {
+ if (!assets || (Array.isArray(assets) && assets.length === 0)) {
+ return;
+ }
+
+ const files = Array.isArray(assets) ? assets : [assets];
+ const imageData = await Promise.all(
+ files.map(async (asset) => {
+ const response = await fetch(asset.uri);
+ const blob = await response.blob();
+ return {
+ blob,
+ mimeType: asset.mimeType || 'image/jpeg',
+ uri: asset.uri
+ };
+ })
+ );
+
+ setNewImages(prev => [...prev, ...imageData]);
+ },
multiple: true,
});
// Calculate which existing images were deleted
const deletedImages = existingImages.filter(img => !existingImagesState.includes(img));
+ // Display images for ImageUploader component
+ const displayImages = newImages.map(img => ({ uri: img.uri }));
+
return (
onSubmit(values, images, deletedImages)}
+ onSubmit={async (values) => {
+ try {
+ let imageKeys: string[] = [];
+
+ // Upload new images if any
+ if (newImages.length > 0) {
+ const result = await upload({
+ images: newImages.map(img => ({ blob: img.blob, mimeType: img.mimeType })),
+ contextString: 'product_info',
+ });
+ imageKeys = result.keys;
+ }
+
+ onSubmit(values, imageKeys, deletedImages);
+ } catch (error) {
+ Alert.alert('Error', error instanceof Error ? error.message : 'Failed to upload images');
+ }
+ }}
enableReinitialize
>
{({ handleChange, handleSubmit, values, setFieldValue, resetForm }) => {
// Clear form when screen comes into focus
const clearForm = useCallback(() => {
- setImages([]);
+ setNewImages([]);
setExistingImagesState([]);
resetForm();
}, [resetForm]);
@@ -143,9 +184,9 @@ const ProductForm = forwardRef(({
{mode === 'create' && (
setImages(prev => prev.filter(img => img.uri !== uri))}
+ onRemoveImage={(uri) => setNewImages(prev => prev.filter(img => img.uri !== uri))}
/>
)}
@@ -166,9 +207,9 @@ const ProductForm = forwardRef(({
Add New Images
setImages(prev => prev.filter(img => img.uri !== uri))}
+ onRemoveImage={(uri) => setNewImages(prev => prev.filter(img => img.uri !== uri))}
/>
)}
@@ -355,11 +396,11 @@ const ProductForm = forwardRef(({
- {isLoading ? (mode === 'create' ? 'Creating...' : 'Updating...') : (mode === 'create' ? 'Create Product' : 'Update Product')}
+ {isUploading ? 'Uploading Images...' : isLoading ? (mode === 'create' ? 'Creating...' : 'Updating...') : (mode === 'create' ? 'Create Product' : 'Update Product')}
@@ -371,4 +412,4 @@ const ProductForm = forwardRef(({
ProductForm.displayName = 'ProductForm';
-export default ProductForm;
\ No newline at end of file
+export default ProductForm;
diff --git a/apps/backend/src/apis/admin-apis/apis/av-router.ts b/apps/backend/src/apis/admin-apis/apis/av-router.ts
index 732e56f..89685a5 100755
--- a/apps/backend/src/apis/admin-apis/apis/av-router.ts
+++ b/apps/backend/src/apis/admin-apis/apis/av-router.ts
@@ -1,6 +1,5 @@
import { Router } from "express";
import { authenticateStaff } from "@/src/middleware/staff-auth";
-import productRouter from "@/src/apis/admin-apis/apis/product.router"
import tagRouter from "@/src/apis/admin-apis/apis/tag.router"
const router = Router();
@@ -8,9 +7,6 @@ const router = Router();
// Apply staff authentication to all admin routes
router.use(authenticateStaff);
-// Product routes
-router.use("/products", productRouter);
-
// Tag routes
router.use("/product-tags", tagRouter);
diff --git a/apps/backend/src/apis/admin-apis/apis/product.controller.ts b/apps/backend/src/apis/admin-apis/apis/product.controller.ts
deleted file mode 100644
index 1ff6783..0000000
--- a/apps/backend/src/apis/admin-apis/apis/product.controller.ts
+++ /dev/null
@@ -1,306 +0,0 @@
-import { Request, Response } from "express";
-import { db } from "@/src/db/db_index";
-import { productInfo, units, specialDeals, productTags } from "@/src/db/schema";
-import { eq, inArray } from "drizzle-orm";
-import { ApiError } from "@/src/lib/api-error";
-import { imageUploadS3, getOriginalUrlFromSignedUrl } from "@/src/lib/s3-client";
-import { deleteS3Image } from "@/src/lib/delete-image";
-import type { SpecialDeal } from "@/src/db/types";
-import { scheduleStoreInitialization } from '@/src/stores/store-initializer';
-
-
-type CreateDeal = {
- quantity: number;
- price: number;
- validTill: string;
-};
-
-/**
- * Create a new product
- */
-export const createProduct = async (req: Request, res: Response) => {
- const { name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, deals, tagIds } = req.body;
-
- // Validate required fields
- if (!name || !unitId || !storeId || !price) {
- throw new ApiError("Name, unitId, storeId, and price are required", 400);
- }
-
- // Check for duplicate name
- const existingProduct = await db.query.productInfo.findFirst({
- where: eq(productInfo.name, name.trim()),
- });
-
- if (existingProduct) {
- throw new ApiError("A product with this name already exists", 400);
- }
-
- // Check if unit exists
- const unit = await db.query.units.findFirst({
- where: eq(units.id, unitId),
- });
-
- if (!unit) {
- throw new ApiError("Invalid unit ID", 400);
- }
-
- // Extract images from req.files
- const images = (req.files as Express.Multer.File[])?.filter(item => item.fieldname === 'images');
- let uploadedImageUrls: string[] = [];
-
- if (images && Array.isArray(images)) {
- const imageUploadPromises = images.map((file, index) => {
- const key = `product-images/${Date.now()}-${index}`;
- return imageUploadS3(file.buffer, file.mimetype, key);
- });
-
- uploadedImageUrls = await Promise.all(imageUploadPromises);
- }
-
- // Create product
- const productData: any = {
- name,
- shortDescription,
- longDescription,
- unitId,
- storeId,
- price,
- marketPrice,
- incrementStep: incrementStep || 1,
- productQuantity: productQuantity || 1,
- isSuspended: isSuspended || false,
- isFlashAvailable: isFlashAvailable || false,
- images: uploadedImageUrls,
- };
-
- if (flashPrice) {
- productData.flashPrice = parseFloat(flashPrice);
- }
-
- const [newProduct] = await db
- .insert(productInfo)
- .values(productData)
- .returning();
-
- // Handle deals if provided
- let createdDeals: SpecialDeal[] = [];
- if (deals && Array.isArray(deals)) {
- const dealInserts = deals.map((deal: CreateDeal) => ({
- productId: newProduct.id,
- quantity: deal.quantity.toString(),
- price: deal.price.toString(),
- validTill: new Date(deal.validTill),
- }));
-
- createdDeals = await db
- .insert(specialDeals)
- .values(dealInserts)
- .returning();
- }
-
- // Handle tag assignments if provided
- if (tagIds && Array.isArray(tagIds)) {
- const tagAssociations = tagIds.map((tagId: number) => ({
- productId: newProduct.id,
- tagId,
- }));
-
- await db.insert(productTags).values(tagAssociations);
- }
-
- // Reinitialize stores to reflect changes
- scheduleStoreInitialization()
-
- // Send response first
- res.status(201).json({
- product: newProduct,
- deals: createdDeals,
- message: "Product created successfully",
- });
-};
-
-/**
- * Update a product
- */
-export const updateProduct = async (req: Request, res: Response) => {
- const id = req.params.id as string
- const { name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, deals:dealsRaw, imagesToDelete:imagesToDeleteRaw, tagIds } = req.body;
-
-
- const deals = dealsRaw ? JSON.parse(dealsRaw) : null;
- const imagesToDelete = imagesToDeleteRaw ? JSON.parse(imagesToDeleteRaw) : [];
-
- if (!name || !unitId || !storeId || !price) {
- throw new ApiError("Name, unitId, storeId, and price are required", 400);
- }
-
- // Check if unit exists
- const unit = await db.query.units.findFirst({
- where: eq(units.id, unitId),
- });
-
- if (!unit) {
- throw new ApiError("Invalid unit ID", 400);
- }
-
- // Get current product to handle image updates
- const currentProduct = await db.query.productInfo.findFirst({
- where: eq(productInfo.id, parseInt(id)),
- });
-
- if (!currentProduct) {
- throw new ApiError("Product not found", 404);
- }
-
- // Handle image deletions
- let currentImages = (currentProduct.images as string[]) || [];
- if (imagesToDelete && imagesToDelete.length > 0) {
- // Convert signed URLs to original S3 URLs for comparison
- const originalUrlsToDelete = imagesToDelete
- .map((signedUrl: string) => getOriginalUrlFromSignedUrl(signedUrl))
- .filter(Boolean); // Remove nulls
-
- // Find which stored images match the ones to delete
- const imagesToRemoveFromDb = currentImages.filter(storedUrl =>
- originalUrlsToDelete.includes(storedUrl)
- );
-
- // Delete the matching images from S3
- const deletePromises = imagesToRemoveFromDb.map(imageUrl => deleteS3Image(imageUrl));
- await Promise.all(deletePromises);
-
- // Remove deleted images from current images array
- currentImages = currentImages.filter(img => !imagesToRemoveFromDb.includes(img));
- }
-
- // Extract new images from req.files
- const images = (req.files as Express.Multer.File[])?.filter(item => item.fieldname === 'images');
- let uploadedImageUrls: string[] = [];
-
- if (images && Array.isArray(images)) {
- const imageUploadPromises = images.map((file, index) => {
- const key = `product-images/${Date.now()}-${index}`;
- return imageUploadS3(file.buffer, file.mimetype, key);
- });
-
- uploadedImageUrls = await Promise.all(imageUploadPromises);
- }
-
- // Combine remaining current images with new uploaded images
- const finalImages = [...currentImages, ...uploadedImageUrls];
-
- const updateData: any = {
- name,
- shortDescription,
- longDescription,
- unitId,
- storeId,
- price,
- marketPrice,
- incrementStep: incrementStep || 1,
- productQuantity: productQuantity || 1,
- isSuspended: isSuspended || false,
- images: finalImages.length > 0 ? finalImages : undefined,
- };
-
- if (isFlashAvailable !== undefined) {
- updateData.isFlashAvailable = isFlashAvailable;
- }
-
- if (flashPrice !== undefined) {
- updateData.flashPrice = flashPrice ? parseFloat(flashPrice) : null;
- }
-
- const [updatedProduct] = await db
- .update(productInfo)
- .set(updateData)
- .where(eq(productInfo.id, parseInt(id)))
- .returning();
-
- if (!updatedProduct) {
- throw new ApiError("Product not found", 404);
- }
-
- // Handle deals if provided
- if (deals && Array.isArray(deals)) {
- // Get existing deals
- const existingDeals = await db.query.specialDeals.findMany({
- where: eq(specialDeals.productId, parseInt(id)),
- });
-
- // Create maps for comparison
- const existingDealsMap = new Map(existingDeals.map(deal => [`${deal.quantity}-${deal.price}`, deal]));
- const newDealsMap = new Map(deals.map((deal: CreateDeal) => [`${deal.quantity}-${deal.price}`, deal]));
-
- // Find deals to add, update, and remove
- const dealsToAdd = deals.filter((deal: CreateDeal) => {
- const key = `${deal.quantity}-${deal.price}`;
- return !existingDealsMap.has(key);
- });
-
- const dealsToRemove = existingDeals.filter(deal => {
- const key = `${deal.quantity}-${deal.price}`;
- return !newDealsMap.has(key);
- });
-
- const dealsToUpdate = deals.filter((deal: CreateDeal) => {
- const key = `${deal.quantity}-${deal.price}`;
- const existing = existingDealsMap.get(key);
- return existing && existing.validTill.toISOString().split('T')[0] !== deal.validTill;
- });
-
- // Remove old deals
- if (dealsToRemove.length > 0) {
- await db.delete(specialDeals).where(
- inArray(specialDeals.id, dealsToRemove.map(deal => deal.id))
- );
- }
-
- // Add new deals
- if (dealsToAdd.length > 0) {
- const dealInserts = dealsToAdd.map((deal: CreateDeal) => ({
- productId: parseInt(id),
- quantity: deal.quantity.toString(),
- price: deal.price.toString(),
- validTill: new Date(deal.validTill),
- }));
- await db.insert(specialDeals).values(dealInserts);
- }
-
- // Update existing deals
- for (const deal of dealsToUpdate) {
- const key = `${deal.quantity}-${deal.price}`;
- const existingDeal = existingDealsMap.get(key);
- if (existingDeal) {
- await db.update(specialDeals)
- .set({ validTill: new Date(deal.validTill) })
- .where(eq(specialDeals.id, existingDeal.id));
- }
- }
- }
-
- // Handle tag assignments if provided
- // if (tagIds && Array.isArray(tagIds)) {
- if (tagIds && Boolean(tagIds)) {
- // Remove existing tags
- await db.delete(productTags).where(eq(productTags.productId, parseInt(id)));
-
- const tagIdsArray = Array.isArray(tagIds) ? tagIds : [+tagIds]
- // Add new tags
- const tagAssociations = tagIdsArray.map((tagId: number) => ({
- productId: parseInt(id),
- tagId,
- }));
-
- await db.insert(productTags).values(tagAssociations);
- }
-
- // Reinitialize stores to reflect changes
- scheduleStoreInitialization()
-
- // Send response first
- res.status(200).json({
- product: updatedProduct,
- message: "Product updated successfully",
- });
-};
diff --git a/apps/backend/src/apis/admin-apis/apis/product.router.ts b/apps/backend/src/apis/admin-apis/apis/product.router.ts
deleted file mode 100644
index fe0bbd1..0000000
--- a/apps/backend/src/apis/admin-apis/apis/product.router.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Router } from "express";
-import { createProduct, updateProduct } from "@/src/apis/admin-apis/apis/product.controller"
-import uploadHandler from '@/src/lib/upload-handler';
-
-const router = Router();
-
-// Product routes
-router.post("/", uploadHandler.array('images'), createProduct);
-router.put("/:id", uploadHandler.array('images'), updateProduct);
-
-export default router;
\ No newline at end of file
diff --git a/apps/backend/src/lib/delete-image.ts b/apps/backend/src/lib/delete-image.ts
index dd6dfdd..3edcfeb 100644
--- a/apps/backend/src/lib/delete-image.ts
+++ b/apps/backend/src/lib/delete-image.ts
@@ -1,7 +1,7 @@
import { eq } from "drizzle-orm";
import { db } from "@/src/db/db_index"
import { deleteImageUtil, getOriginalUrlFromSignedUrl } from "@/src/lib/s3-client"
-import { s3Url } from "@/src/lib/env-exporter"
+import { assetsDomain, s3Url } from "@/src/lib/env-exporter"
function extractS3Key(url: string): string | null {
try {
@@ -27,12 +27,22 @@ function extractS3Key(url: string): string | null {
export async function deleteS3Image(imageUrl: string) {
try {
- // First check if this is a signed URL and get the original if it is
- const originalUrl = getOriginalUrlFromSignedUrl(imageUrl) || imageUrl;
-
- const key = extractS3Key(originalUrl || "");
-
+ let key:string | null = '';
+
+ if(imageUrl.includes(assetsDomain)) {
+ key = imageUrl.replace(assetsDomain, '')
+ }
+ else if(imageUrl.startsWith('http')){
+ // First check if this is a signed URL and get the original if it is
+ const originalUrl = getOriginalUrlFromSignedUrl(imageUrl) || imageUrl;
+
+ key = extractS3Key(originalUrl || "");
+ }
+
+ else {
+ key = imageUrl;
+ }
if (!key) {
throw new Error("Invalid image URL format");
}
diff --git a/apps/backend/src/lib/s3-client.ts b/apps/backend/src/lib/s3-client.ts
index 7862169..58a8c01 100755
--- a/apps/backend/src/lib/s3-client.ts
+++ b/apps/backend/src/lib/s3-client.ts
@@ -201,9 +201,12 @@ export function extractKeyFromPresignedUrl(url: string): string {
export async function claimUploadUrl(url: string): Promise {
try {
- const semiKey = extractKeyFromPresignedUrl(url);
- const key = s3BucketName+'/'+ semiKey
-
+ let semiKey:string = ''
+
+ if(url.startsWith('http'))
+ semiKey = extractKeyFromPresignedUrl(url);
+ else
+ semiKey = url
// Update status to 'claimed' if currently 'pending'
const result = await db
.update(uploadUrlStatus)
diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/product.ts b/apps/backend/src/trpc/apis/admin-apis/apis/product.ts
index d72d7bc..4677c40 100644
--- a/apps/backend/src/trpc/apis/admin-apis/apis/product.ts
+++ b/apps/backend/src/trpc/apis/admin-apis/apis/product.ts
@@ -110,6 +110,229 @@ export const productRouter = router({
};
}),
+ createProduct: protectedProcedure
+ .input(z.object({
+ name: z.string().min(1),
+ shortDescription: z.string().optional(),
+ longDescription: z.string().optional(),
+ unitId: z.number(),
+ storeId: z.number(),
+ price: z.number(),
+ marketPrice: z.number().optional(),
+ incrementStep: z.number().default(1),
+ productQuantity: z.number().default(1),
+ isSuspended: z.boolean().default(false),
+ isFlashAvailable: z.boolean().default(false),
+ flashPrice: z.number().optional(),
+ deals: z.array(z.object({
+ quantity: z.number(),
+ price: z.number(),
+ validTill: z.string(),
+ })).optional(),
+ tagIds: z.array(z.number()).optional(),
+ imageKeys: z.array(z.string()).optional(),
+ }))
+ .mutation(async ({ input }) => {
+ const {
+ name, shortDescription, longDescription, unitId, storeId,
+ price, marketPrice, incrementStep, productQuantity,
+ isSuspended, isFlashAvailable, flashPrice,
+ deals, tagIds, imageKeys
+ } = input;
+
+ // Validation
+ if (!name || !unitId || !storeId || !price) {
+ throw new ApiError("Name, unitId, storeId, and price are required", 400);
+ }
+
+ // Check for duplicate name
+ const existingProduct = await db.query.productInfo.findFirst({
+ where: eq(productInfo.name, name.trim()),
+ });
+ if (existingProduct) {
+ throw new ApiError("A product with this name already exists", 400);
+ }
+
+ // Check if unit exists
+ const unit = await db.query.units.findFirst({
+ where: eq(units.id, unitId),
+ });
+ if (!unit) {
+ throw new ApiError("Invalid unit ID", 400);
+ }
+
+ console.log(imageKeys)
+ const [newProduct] = await db
+ .insert(productInfo)
+ .values({
+ name: name.trim(),
+ shortDescription,
+ longDescription,
+ unitId,
+ storeId,
+ price: price.toString(),
+ marketPrice: marketPrice?.toString(),
+ incrementStep,
+ productQuantity,
+ isSuspended,
+ isFlashAvailable,
+ flashPrice: flashPrice?.toString(),
+ images: imageKeys || [],
+ })
+ .returning();
+
+ // Handle deals
+ if (deals && deals.length > 0) {
+ const dealInserts = deals.map(deal => ({
+ productId: newProduct.id,
+ quantity: deal.quantity.toString(),
+ price: deal.price.toString(),
+ validTill: new Date(deal.validTill),
+ }));
+ await db.insert(specialDeals).values(dealInserts);
+ }
+
+ // Handle tags
+ if (tagIds && tagIds.length > 0) {
+ const tagAssociations = tagIds.map(tagId => ({
+ productId: newProduct.id,
+ tagId,
+ }));
+ await db.insert(productTags).values(tagAssociations);
+ }
+
+ // Claim upload URLs
+ if (imageKeys && imageKeys.length > 0) {
+ for (const key of imageKeys) {
+ try {
+ await claimUploadUrl(key);
+ } catch (e) {
+ console.warn(`Failed to claim upload URL for key: ${key}`, e);
+ }
+ }
+ }
+
+ scheduleStoreInitialization();
+
+ return {
+ product: newProduct,
+ message: "Product created successfully",
+ };
+ }),
+
+ updateProduct: protectedProcedure
+ .input(z.object({
+ id: z.number(),
+ name: z.string().min(1).optional(),
+ shortDescription: z.string().optional(),
+ longDescription: z.string().optional(),
+ unitId: z.number().optional(),
+ storeId: z.number().optional(),
+ price: z.number().optional(),
+ marketPrice: z.number().optional(),
+ incrementStep: z.number().optional(),
+ productQuantity: z.number().optional(),
+ isSuspended: z.boolean().optional(),
+ isFlashAvailable: z.boolean().optional(),
+ flashPrice: z.number().optional(),
+ deals: z.array(z.object({
+ quantity: z.number(),
+ price: z.number(),
+ validTill: z.string(),
+ })).optional(),
+ tagIds: z.array(z.number()).optional(),
+ newImageKeys: z.array(z.string()).optional(),
+ imagesToDelete: z.array(z.string()).optional(),
+ }))
+ .mutation(async ({ input }) => {
+ const { id, newImageKeys, imagesToDelete, deals, tagIds, ...updateData } = input;
+
+ // Get current product
+ const currentProduct = await db.query.productInfo.findFirst({
+ where: eq(productInfo.id, id),
+ });
+ if (!currentProduct) {
+ throw new ApiError("Product not found", 404);
+ }
+
+ // Handle image deletions
+ let currentImages = (currentProduct.images as string[]) || [];
+ if (imagesToDelete && imagesToDelete.length > 0) {
+ for (const imageUrl of imagesToDelete) {
+ try {
+ await deleteS3Image(imageUrl);
+ } catch (e) {
+ console.error(`Failed to delete image: ${imageUrl}`, e);
+ }
+ }
+ currentImages = currentImages.filter(img => {
+ //!imagesToDelete.includes(img)
+ const isRemoved = imagesToDelete.some(item => item.includes(img));
+ return !isRemoved;
+ });
+ }
+
+ // Add new images
+ if (newImageKeys && newImageKeys.length > 0) {
+ currentImages = [...currentImages, ...newImageKeys];
+
+ for (const key of newImageKeys) {
+ try {
+ await claimUploadUrl(key);
+ } catch (e) {
+ console.warn(`Failed to claim upload URL for key: ${key}`, e);
+ }
+ }
+ }
+
+ // Update product - convert numeric fields to strings for PostgreSQL numeric type
+ const { price, marketPrice, flashPrice, ...otherData } = updateData;
+ const [updatedProduct] = await db
+ .update(productInfo)
+ .set({
+ ...otherData,
+ ...(price !== undefined && { price: price.toString() }),
+ ...(marketPrice !== undefined && { marketPrice: marketPrice.toString() }),
+ ...(flashPrice !== undefined && { flashPrice: flashPrice.toString() }),
+ images: currentImages,
+ })
+ .where(eq(productInfo.id, id))
+ .returning();
+
+ // Handle deals update
+ if (deals !== undefined) {
+ await db.delete(specialDeals).where(eq(specialDeals.productId, id));
+ if (deals.length > 0) {
+ const dealInserts = deals.map(deal => ({
+ productId: id,
+ quantity: deal.quantity.toString(),
+ price: deal.price.toString(),
+ validTill: new Date(deal.validTill),
+ }));
+ await db.insert(specialDeals).values(dealInserts);
+ }
+ }
+
+ // Handle tags update
+ if (tagIds !== undefined) {
+ await db.delete(productTags).where(eq(productTags.productId, id));
+ if (tagIds.length > 0) {
+ const tagAssociations = tagIds.map(tagId => ({
+ productId: id,
+ tagId,
+ }));
+ await db.insert(productTags).values(tagAssociations);
+ }
+ }
+
+ scheduleStoreInitialization();
+
+ return {
+ product: updatedProduct,
+ message: "Product updated successfully",
+ };
+ }),
+
toggleOutOfStock: protectedProcedure
.input(z.object({
id: z.number(),