food scription and food contents
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
VITE_TOKEN_NAME = 'dmnu_a_t'
|
||||
VITE_REFRESH_TOKEN_NAME = 'dmnu-a-rt'
|
||||
|
||||
VITE_BASE_URL = 'https://dmenu-api.danakcorp.com'
|
||||
#VITE_BASE_URL = 'http://192.168.1.107:2000'
|
||||
# VITE_BASE_URL = 'https://dmenu-api.danakcorp.com'
|
||||
VITE_BASE_URL = 'http://192.168.99.131:2000'
|
||||
|
||||
VITE_SOCKET_URL = 'https://dmenu-api.danakcorp.com'
|
||||
VITE_INVOICE_URL = 'https://console.danakcorp.com/receipts/'
|
||||
+6
-159
@@ -1,8 +1,4 @@
|
||||
import Button from "@/components/Button";
|
||||
import Input from "@/components/Input";
|
||||
import Select from "@/components/Select";
|
||||
import Textarea from "@/components/Textarea";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import type { ErrorType } from "@/helpers/types";
|
||||
import { useFormik } from "formik";
|
||||
import { TickCircle } from "iconsax-react";
|
||||
@@ -11,8 +7,11 @@ import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import * as Yup from "yup";
|
||||
import { useMultipleUpload } from "../uploader/hooks/useUploaderData";
|
||||
import BasicInfoSection from "./components/BasicInfoSection";
|
||||
import CreateFoodSidebar from "./components/CreateFoodSidebar";
|
||||
import FoodContentSection from "./components/FoodContentSection";
|
||||
import MealTimesSection from "./components/MealTimesSection";
|
||||
import ServiceOptionsSection from "./components/ServiceOptionsSection";
|
||||
import WeekDaysSection from "./components/WeekDaysSection";
|
||||
import { useCreateFood, useGetCategories } from "./hooks/useFoodData";
|
||||
import type { CreateFoodType } from "./types/Types";
|
||||
@@ -117,163 +116,11 @@ const CreateFood: FC = () => {
|
||||
<div className="flex flex-col lg:flex-row gap-6 mt-6">
|
||||
<div className="flex-1">
|
||||
<div className="bg-white py-6 sm:py-8 xl:px-10 px-4 rounded-3xl">
|
||||
<Input
|
||||
label="نام غذا"
|
||||
name="title"
|
||||
placeholder=""
|
||||
value={formik.values.title}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ""}
|
||||
/>
|
||||
|
||||
<div className="mt-5">
|
||||
<Select
|
||||
items={categories}
|
||||
label="دسته بندی"
|
||||
placeholder="انتخاب کنید"
|
||||
name="categoryId"
|
||||
value={formik.values.categoryId}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
formik.setFieldValue("categoryId", value);
|
||||
}}
|
||||
error_text={formik.touched.categoryId && formik.errors.categoryId ? String(formik.errors.categoryId) : ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 mt-5">
|
||||
<Input
|
||||
name="price"
|
||||
label="قیمت"
|
||||
placeholder="تومان"
|
||||
type="number"
|
||||
seprator={true}
|
||||
value={formik.values.price}
|
||||
onChange={(e) => formik.setFieldValue("price", Number(e.target.value))}
|
||||
error_text={formik.touched.price && formik.errors.price ? formik.errors.price : ""}
|
||||
/>
|
||||
|
||||
<Input
|
||||
name="discount"
|
||||
label="تخفیف"
|
||||
placeholder="تومان"
|
||||
type="number"
|
||||
seprator={true}
|
||||
value={formik.values.discount}
|
||||
onChange={(e) => formik.setFieldValue("discount", Number(e.target.value))}
|
||||
error_text={formik.touched.discount && formik.errors.discount ? formik.errors.discount : ""}
|
||||
/>
|
||||
|
||||
<Input
|
||||
name="prepareTime"
|
||||
label="زمان آماده سازی"
|
||||
placeholder="دقیقه"
|
||||
type="number"
|
||||
value={formik.values.prepareTime}
|
||||
onChange={(e) => formik.setFieldValue("prepareTime", Number(e.target.value))}
|
||||
error_text={formik.touched.prepareTime && formik.errors.prepareTime ? formik.errors.prepareTime : ""}
|
||||
/>
|
||||
|
||||
<Input
|
||||
name="dailyStock"
|
||||
label="موجودی روزانه"
|
||||
placeholder="عدد"
|
||||
type="number"
|
||||
seprator={true}
|
||||
value={formik.values.dailyStock}
|
||||
onChange={(e) => formik.setFieldValue("dailyStock", Number(e.target.value))}
|
||||
error_text={formik.touched.dailyStock && formik.errors.dailyStock ? formik.errors.dailyStock : ""}
|
||||
/>
|
||||
|
||||
<Input
|
||||
name="availableStock"
|
||||
label="موجودی فعلی"
|
||||
placeholder="عدد"
|
||||
type="number"
|
||||
seprator={true}
|
||||
value={formik.values.availableStock}
|
||||
onChange={(e) => formik.setFieldValue("availableStock", Number(e.target.value))}
|
||||
error_text={formik.touched.availableStock && formik.errors.availableStock ? formik.errors.availableStock : ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<Textarea
|
||||
name="desc"
|
||||
label="توضیحات غذا"
|
||||
placeholder=""
|
||||
value={formik.values.desc}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.desc && formik.errors.desc ? formik.errors.desc : ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<Input
|
||||
name="order"
|
||||
label="اولویت"
|
||||
placeholder="عدد"
|
||||
type="number"
|
||||
value={formik.values.order}
|
||||
onChange={(e) => formik.setFieldValue("order", Number(e.target.value))}
|
||||
error_text={formik.touched.order && formik.errors.order ? formik.errors.order : ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<div className="text-sm mb-2">محتوای غذا</div>
|
||||
<div className="space-y-2">
|
||||
{formik.values.content.map((item, index) => (
|
||||
<div key={index} className="flex gap-2 items-center">
|
||||
<Input
|
||||
placeholder="متن محتوا"
|
||||
value={item}
|
||||
onChange={(e) => {
|
||||
const newContent = [...formik.values.content];
|
||||
newContent[index] = e.target.value;
|
||||
formik.setFieldValue("content", newContent);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
className="px-3 bg-red-500 hover:bg-red-600 w-auto"
|
||||
onClick={() => {
|
||||
const newContent = formik.values.content.filter((_, i) => i !== index);
|
||||
formik.setFieldValue("content", newContent);
|
||||
}}
|
||||
>
|
||||
حذف
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
className="w-full bg-gray-200 text-gray-700 hover:bg-gray-300"
|
||||
onClick={() => {
|
||||
formik.setFieldValue("content", [...formik.values.content, ""]);
|
||||
}}
|
||||
>
|
||||
افزودن محتوا
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BasicInfoSection formik={formik} categories={categories} />
|
||||
<FoodContentSection formik={formik} />
|
||||
<MealTimesSection formik={formik} />
|
||||
<WeekDaysSection formik={formik} />
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="text-sm mb-3">گزینههای سرویس</div>
|
||||
<div className="flex gap-6 flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox checked={formik.values.pickupServe} onCheckedChange={(checked) => formik.setFieldValue("pickupServe", checked)} />
|
||||
<label className="text-xs cursor-pointer"> بیرونبر</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox checked={formik.values.inPlaceServe} onCheckedChange={(checked) => formik.setFieldValue("inPlaceServe", checked)} />
|
||||
<label className="text-xs cursor-pointer">سرو در محل</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ServiceOptionsSection formik={formik} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,16 +1,53 @@
|
||||
import { type FC } from 'react'
|
||||
import type { FormikProps } from 'formik'
|
||||
import { Magicpen } from 'iconsax-react'
|
||||
import Input from '@/components/Input'
|
||||
import Select from '@/components/Select'
|
||||
import Textarea from '@/components/Textarea'
|
||||
import Button from '@/components/Button'
|
||||
import { extractErrorMessage } from '@/config/func'
|
||||
import { toast } from 'react-toastify'
|
||||
import type { CreateFoodType } from '../types/Types'
|
||||
import { useGenerateFoodDescription } from '../hooks/useFoodData'
|
||||
|
||||
type BasicInfoSectionProps = {
|
||||
formik: FormikProps<CreateFoodType>
|
||||
categories: Array<{ label: string; value: string }>
|
||||
}
|
||||
|
||||
const parseDescription = (data: { answer?: string } | undefined): string => {
|
||||
return data?.answer?.trim() || ''
|
||||
}
|
||||
|
||||
const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories }) => {
|
||||
const { mutate: generateDescription, isPending: isGeneratingDesc } = useGenerateFoodDescription()
|
||||
|
||||
const handleGenerateDescription = () => {
|
||||
const foodName = formik.values.title.trim()
|
||||
if (!foodName) {
|
||||
toast.error('لطفاً ابتدا نام کامل غذا را وارد کنید')
|
||||
return
|
||||
}
|
||||
|
||||
generateDescription(
|
||||
{ foodName },
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
const description = parseDescription(response.data)
|
||||
if (!description) {
|
||||
toast.error('توضیحی از هوش مصنوعی دریافت نشد')
|
||||
return
|
||||
}
|
||||
formik.setFieldValue('desc', description)
|
||||
toast.success('توضیحات با موفقیت تولید شد')
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(extractErrorMessage(error))
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
@@ -94,9 +131,22 @@ const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories }) =>
|
||||
</div>
|
||||
|
||||
<div className='mt-5'>
|
||||
<div className='flex items-center justify-between mb-1'>
|
||||
<div className='text-sm'>توضیحات غذا</div>
|
||||
<Button
|
||||
type='button'
|
||||
className='w-auto h-8 px-3 bg-transparent text-primary border border-primary text-xs gap-1.5'
|
||||
onClick={handleGenerateDescription}
|
||||
isloading={isGeneratingDesc}
|
||||
colorLoading='black'
|
||||
disabled={!formik.values.title.trim() || isGeneratingDesc}
|
||||
>
|
||||
<Magicpen size={16} color='currentColor' />
|
||||
تولید با AI
|
||||
</Button>
|
||||
</div>
|
||||
<Textarea
|
||||
name='desc'
|
||||
label='توضیحات غذا'
|
||||
placeholder=''
|
||||
value={formik.values.desc}
|
||||
onChange={formik.handleChange}
|
||||
@@ -120,4 +170,3 @@ const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories }) =>
|
||||
}
|
||||
|
||||
export default BasicInfoSection
|
||||
|
||||
|
||||
@@ -1,18 +1,70 @@
|
||||
import { type FC } from 'react'
|
||||
import type { FormikProps } from 'formik'
|
||||
import { Magicpen } from 'iconsax-react'
|
||||
import Input from '@/components/Input'
|
||||
import Button from '@/components/Button'
|
||||
import { extractErrorMessage } from '@/config/func'
|
||||
import { toast } from 'react-toastify'
|
||||
import type { CreateFoodType } from '../types/Types'
|
||||
import { useGenerateFoodContent } from '../hooks/useFoodData'
|
||||
|
||||
type FoodContentSectionProps = {
|
||||
formik: FormikProps<CreateFoodType>
|
||||
}
|
||||
|
||||
const parseContent = (data: { answer?: string } | undefined): string[] => {
|
||||
if (!data?.answer) return []
|
||||
return data.answer
|
||||
.split('،')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
const FoodContentSection: FC<FoodContentSectionProps> = ({ formik }) => {
|
||||
const { mutate: generateContent, isPending: isGeneratingContent } = useGenerateFoodContent()
|
||||
|
||||
const handleGenerateContent = () => {
|
||||
const foodName = formik.values.title.trim()
|
||||
if (!foodName) {
|
||||
toast.error('لطفاً ابتدا نام کامل غذا را وارد کنید')
|
||||
return
|
||||
}
|
||||
|
||||
generateContent(
|
||||
{ foodName },
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
const content = parseContent(response.data)
|
||||
if (!content.length) {
|
||||
toast.error('محتوایی از هوش مصنوعی دریافت نشد')
|
||||
return
|
||||
}
|
||||
formik.setFieldValue('content', content)
|
||||
toast.success('محتوای غذا با موفقیت تولید شد')
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(extractErrorMessage(error))
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mt-5'>
|
||||
<div className='text-sm mb-2'>محتوای غذا</div>
|
||||
<div className='flex items-center justify-between mb-2'>
|
||||
<div className='text-sm'>محتوای غذا</div>
|
||||
<Button
|
||||
type='button'
|
||||
className='w-auto h-8 px-3 bg-transparent text-primary border border-primary text-xs gap-1.5'
|
||||
onClick={handleGenerateContent}
|
||||
isloading={isGeneratingContent}
|
||||
colorLoading='black'
|
||||
disabled={!formik.values.title.trim() || isGeneratingContent}
|
||||
>
|
||||
<Magicpen size={16} color='currentColor' />
|
||||
تولید با AI
|
||||
</Button>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
{formik.values.content.map((item, index) => (
|
||||
<div key={index} className='flex gap-2 items-center'>
|
||||
|
||||
@@ -113,3 +113,15 @@ export const useSetStockForFood = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useGenerateFoodDescription = () => {
|
||||
return useMutation({
|
||||
mutationFn: api.generateFoodDescription,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGenerateFoodContent = () => {
|
||||
return useMutation({
|
||||
mutationFn: api.generateFoodContent,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,6 +2,9 @@ import axios from "@/config/axios";
|
||||
import type {
|
||||
CreateCategoryType,
|
||||
CreateFoodType,
|
||||
GenerateFoodAiParams,
|
||||
GenerateFoodContentResponse,
|
||||
GenerateFoodDescriptionResponse,
|
||||
GetCategoriesResponseType,
|
||||
GetFoodDetailsResponseType,
|
||||
GetFoodsParams,
|
||||
@@ -90,3 +93,23 @@ export const setStockForFood = async (foodId: string, params: SetStockType) => {
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const generateFoodDescription = async (
|
||||
params: GenerateFoodAiParams,
|
||||
): Promise<GenerateFoodDescriptionResponse> => {
|
||||
const { data } = await axios.post<GenerateFoodDescriptionResponse>(
|
||||
`/admin/ai/food-description`,
|
||||
params,
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const generateFoodContent = async (
|
||||
params: GenerateFoodAiParams,
|
||||
): Promise<GenerateFoodContentResponse> => {
|
||||
const { data } = await axios.post<GenerateFoodContentResponse>(
|
||||
`/admin/ai/food-content`,
|
||||
params,
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -155,3 +155,18 @@ export type SetStockType = {
|
||||
totalStock: number;
|
||||
availableStock: number;
|
||||
};
|
||||
|
||||
export type GenerateFoodAiParams = {
|
||||
foodName: string;
|
||||
};
|
||||
|
||||
export type GenerateFoodAiResult = {
|
||||
answer: string;
|
||||
consumedTokens?: number;
|
||||
cost?: number;
|
||||
promptTokens?: number;
|
||||
completionTokens?: number;
|
||||
};
|
||||
|
||||
export type GenerateFoodDescriptionResponse = IResponse<GenerateFoodAiResult>;
|
||||
export type GenerateFoodContentResponse = IResponse<GenerateFoodAiResult>;
|
||||
|
||||
Reference in New Issue
Block a user