diff --git a/src/app/[lang]/(dashboard)/(private)/dashboards/overview/page.tsx b/src/app/[lang]/(dashboard)/(private)/dashboards/overview/page.tsx index 77e61d3..16e0e10 100644 --- a/src/app/[lang]/(dashboard)/(private)/dashboards/overview/page.tsx +++ b/src/app/[lang]/(dashboard)/(private)/dashboards/overview/page.tsx @@ -3,7 +3,7 @@ import React from 'react' import { useDashboardAnalytics } from '../../../../../../services/queries/analytics' import Loading from '../../../../../../components/layout/shared/Loading' -import { formatCurrency, formatDate } from '../../../../../../utils/transform' +import { formatCurrency, formatDate, formatShortCurrency } from '../../../../../../utils/transform' import ProductSales from '../../../../../../views/dashboards/products/ProductSales' import PaymentMethodReport from '../../../../../../views/dashboards/payment-methods/PaymentMethodReport' import OrdersReport from '../../../../../../views/dashboards/orders/OrdersReport' @@ -55,7 +55,7 @@ const DashboardOverview = () => { { { + // Sample data - replace with your actual data + const { data: profitData, isLoading } = useProfitLossAnalytics() + + const formatCurrency = (amount: any) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(amount) } - return nameMap[metric] || metric.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) -} - -const DashboardProfitLoss = () => { - const { data, isLoading } = useProfitLossAnalytics() + const formatPercentage = (value: any) => { + return `${value.toFixed(2)}%` + } const formatDate = (dateString: any) => { return new Date(dateString).toLocaleDateString('id-ID', { + day: 'numeric', month: 'short', - day: 'numeric' + year: 'numeric' }) } - const metrics = ['cost', 'revenue', 'gross_profit', 'net_profit'] - - const transformSalesData = (data: ProfitLossReport) => { - return [ - { - type: 'products', - avatarIcon: 'tabler-package', - date: data.product_data.map((d: ProductDataReport) => d.product_name), - series: [{ data: data.product_data.map((d: ProductDataReport) => d.revenue) }] - } - // { - // type: 'profits', - // avatarIcon: 'tabler-currency-dollar', - // date: data.data.map((d: DailyData) => formatDate(d.date)), - // series: metrics.map(metric => ({ - // name: formatMetricName(metric as string), - // data: data.data.map((item: any) => item[metric] as number) - // })) - // } - ] + const getProfitabilityColor = (margin: any) => { + if (margin > 50) return 'text-green-600 bg-green-100' + if (margin > 0) return 'text-yellow-600 bg-yellow-100' + return 'text-red-600 bg-red-100' } + function formatMetricName(metric: string): string { + const nameMap: { [key: string]: string } = { + revenue: 'Revenue', + net_profit: 'Net Profit', + } + + return nameMap[metric] || metric.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) + } + + const metrics = ['revenue', 'net_profit'] + const transformMultipleData = (data: ProfitLossReport) => { return [ { @@ -75,58 +61,285 @@ const DashboardProfitLoss = () => { ] } - if (isLoading) return + const MetricCard = ({ iconClass, title, value, subtitle, bgColor = 'bg-blue-500', isNegative = false }: any) => ( +
+
+
+

{title}

+

{value}

+ {subtitle &&

{subtitle}

} +
+
+ +
+
+
+ ) return ( - - - - - - - - - - - - - - - - - - - - + <> + {profitData && ( +
+ {/* Header */} +
+

Profit Analysis Dashboard

+

+ {formatDate(profitData.date_from)} - {formatDate(profitData.date_to)} +

+
+ + {/* Summary Metrics */} +
+ + + + +
+ + {/* Additional Summary Metrics */} +
+
+
+ +

Net Profit

+
+

+ {formatShortCurrency(profitData.summary.net_profit)} +

+

Margin: {formatPercentage(profitData.summary.net_profit_margin)}

+
+
+
+ +

Total Orders

+
+

{profitData.summary.total_orders}

+
+
+
+ +

Tax & Discount

+
+

+ {formatShortCurrency(profitData.summary.total_tax + profitData.summary.total_discount)} +

+

+ Tax: {formatShortCurrency(profitData.summary.total_tax)} | Discount:{' '} + {formatShortCurrency(profitData.summary.total_discount)} +

+
+
+ + {/* Profit Chart */} +
+ +
+ +
+ {/* Daily Breakdown */} +
+
+
+ +

Daily Breakdown

+
+
+ + + + + + + + + + + + + {profitData.data.map((day, index) => ( + + + + + + + + + ))} + +
DateRevenueCostProfitMarginOrders
+ {formatDate(day.date)} + + {formatCurrency(day.revenue)} + + {formatCurrency(day.cost)} + = 0 ? 'text-green-600' : 'text-red-600' + }`} + > + {formatCurrency(day.gross_profit)} + + + {formatPercentage(day.gross_profit_margin)} + + {day.orders}
+
+
+
+ + {/* Top Performing Products */} +
+
+
+ +

Top Performers

+
+
+ {profitData.product_data + .sort((a, b) => b.gross_profit - a.gross_profit) + .slice(0, 5) + .map((product, index) => ( +
+
+ + {index + 1} + +
+

{product.product_name}

+

{product.category_name}

+
+
+
+

= 0 ? 'text-green-600' : 'text-red-600'}`}> + {formatCurrency(product.gross_profit)} +

+

{formatPercentage(product.gross_profit_margin)}

+
+
+ ))} +
+
+
+
+ + {/* Product Analysis Table */} +
+
+
+ +

Product Analysis

+
+
+ + + + + + + + + + + + + + + {profitData.product_data + .sort((a, b) => b.gross_profit - a.gross_profit) + .map(product => ( + + + + + + + + + + + ))} + +
ProductCategoryQtyRevenueCostProfitMarginPer Unit
+
{product.product_name}
+
+ + {product.category_name} + + + {product.quantity_sold} + + {formatCurrency(product.revenue)} + + {formatCurrency(product.cost)} + = 0 ? 'text-green-600' : 'text-red-600' + }`} + > + {formatCurrency(product.gross_profit)} + + + {formatPercentage(product.gross_profit_margin)} + + = 0 ? 'text-green-600' : 'text-red-600' + }`} + > + {formatCurrency(product.profit_per_unit)} +
+
+
+
+
+ )} + ) } -export default DashboardProfitLoss +export default DashboardProfitloss diff --git a/src/redux-store/index.ts b/src/redux-store/index.ts index 37c577c..c1f2b49 100644 --- a/src/redux-store/index.ts +++ b/src/redux-store/index.ts @@ -6,6 +6,7 @@ import customerReducer from '@/redux-store/slices/customer' import paymentMethodReducer from '@/redux-store/slices/paymentMethod' import ingredientReducer from '@/redux-store/slices/ingredient' import orderReducer from '@/redux-store/slices/order' +import productRecipeReducer from '@/redux-store/slices/productRecipe' export const store = configureStore({ reducer: { @@ -13,7 +14,8 @@ export const store = configureStore({ customerReducer, paymentMethodReducer, ingredientReducer, - orderReducer + orderReducer, + productRecipeReducer }, middleware: getDefaultMiddleware => getDefaultMiddleware({ serializableCheck: false }) }) diff --git a/src/redux-store/slices/productRecipe.ts b/src/redux-store/slices/productRecipe.ts new file mode 100644 index 0000000..5dc1c7a --- /dev/null +++ b/src/redux-store/slices/productRecipe.ts @@ -0,0 +1,28 @@ +// Third-party Imports +import type { PayloadAction } from '@reduxjs/toolkit' +import { createSlice } from '@reduxjs/toolkit' + +// Type Imports + +// Data Imports + +const initialState: { currentProductRecipe: any } = { + currentProductRecipe: {} +} + +export const productRecipeSlice = createSlice({ + name: 'productRecipe', + initialState, + reducers: { + setProductRecipe: (state, action: PayloadAction) => { + state.currentProductRecipe = action.payload + }, + resetProductRecipe: state => { + state.currentProductRecipe = initialState.currentProductRecipe + } + } +}) + +export const { setProductRecipe, resetProductRecipe } = productRecipeSlice.actions + +export default productRecipeSlice.reducer diff --git a/src/services/mutations/productRecipes.ts b/src/services/mutations/productRecipes.ts new file mode 100644 index 0000000..4ccf3de --- /dev/null +++ b/src/services/mutations/productRecipes.ts @@ -0,0 +1,45 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { toast } from 'react-toastify' +import { ProductRecipeRequest } from '../../types/services/productRecipe' +import { api } from '../api' + +export const useProductRecipesMutation = () => { + const queryClient = useQueryClient() + + const createProductRecipe = useMutation({ + mutationFn: async (newProductRecipe: ProductRecipeRequest) => { + const { variant_id, ...rest } = newProductRecipe + + const cleanRequest = variant_id ? newProductRecipe : rest + + const response = await api.post('/product-recipes', cleanRequest) + return response.data + }, + onSuccess: () => { + toast.success('Product Recipe created successfully!') + queryClient.invalidateQueries({ queryKey: ['product-recipes'] }) + }, + onError: (error: any) => { + toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed') + } + }) + + const updateProductRecipe = useMutation({ + mutationFn: async ({ id, payload }: { id: string; payload: ProductRecipeRequest }) => { + const response = await api.put(`/product-recipes/${id}`, payload) + return response.data + }, + onSuccess: () => { + toast.success('Product Recipe updated successfully!') + queryClient.invalidateQueries({ queryKey: ['product-recipes'] }) + }, + onError: (error: any) => { + toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed') + } + }) + + return { + createProductRecipe, + updateProductRecipe + } +} diff --git a/src/services/queries/productRecipes.ts b/src/services/queries/productRecipes.ts new file mode 100644 index 0000000..aa2f8da --- /dev/null +++ b/src/services/queries/productRecipes.ts @@ -0,0 +1,13 @@ +import { useQuery } from '@tanstack/react-query' +import { ProductRecipe } from '../../types/services/productRecipe' +import { api } from '../api' + +export function useProductRecipesByProduct(productId: string) { + return useQuery({ + queryKey: ['product-recipes', productId], + queryFn: async () => { + const res = await api.get(`/product-recipes/product/${productId}`) + return res.data.data + } + }) +} diff --git a/src/services/queries/products.ts b/src/services/queries/products.ts index cc895f1..c2cf7c8 100644 --- a/src/services/queries/products.ts +++ b/src/services/queries/products.ts @@ -1,6 +1,7 @@ import { useQuery } from '@tanstack/react-query' -import { Products } from '../../types/services/product' +import { Product, Products } from '../../types/services/product' import { api } from '../api' +import { ProductRecipe } from '../../types/services/productRecipe' interface ProductsQueryParams { page?: number @@ -39,7 +40,7 @@ export function useProducts(params: ProductsQueryParams = {}) { } export function useProductById(id: string) { - return useQuery({ + return useQuery({ queryKey: ['product', id], queryFn: async () => { const res = await api.get(`/products/${id}`) diff --git a/src/types/services/productRecipe.ts b/src/types/services/productRecipe.ts new file mode 100644 index 0000000..6a43cc2 --- /dev/null +++ b/src/types/services/productRecipe.ts @@ -0,0 +1,56 @@ +export interface Product { + ID: string; + OrganizationID: string; + CategoryID: string; + SKU: string; + Name: string; + Description: string | null; + Price: number; + Cost: number; + BusinessType: string; + ImageURL: string; + PrinterType: string; + UnitID: string | null; + HasIngredients: boolean; + Metadata: Record; + IsActive: boolean; + CreatedAt: string; // ISO date string + UpdatedAt: string; // ISO date string +} + +export interface Ingredient { + id: string; + organization_id: string; + outlet_id: string | null; + name: string; + unit_id: string; + cost: number; + stock: number; + is_semi_finished: boolean; + is_active: boolean; + metadata: Record; + created_at: string; + updated_at: string; +} + +export interface ProductRecipe { + id: string; + organization_id: string; + outlet_id: string | null; + product_id: string; + variant_id: string | null; + ingredient_id: string; + quantity: number; + created_at: string; + updated_at: string; + product: Product; + ingredient: Ingredient; +} + +export interface ProductRecipeRequest { + product_id: string; + variant_id: string | null; + ingredient_id: string; + quantity: number; + outlet_id: string | null; +} diff --git a/src/utils/transform.ts b/src/utils/transform.ts index df861eb..9c741e4 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -7,12 +7,23 @@ export const formatCurrency = (amount: number) => { } export const formatShortCurrency = (num: number): string => { - if (num >= 1_000_000) { - return (num / 1_000_000).toFixed(2) + 'M' - } else if (num >= 1_000) { - return (num / 1_000).toFixed(2) + 'k' + const formatNumber = (value: number, suffix: string) => { + const str = value.toFixed(2).replace(/\.00$/, '') + return str + suffix } - return num.toString() + + const absNum = Math.abs(num) + let result: string + + if (absNum >= 1_000_000) { + result = formatNumber(absNum / 1_000_000, 'M') + } else if (absNum >= 1_000) { + result = formatNumber(absNum / 1_000, 'k') + } else { + result = absNum.toString() + } + + return num < 0 ? '-' + 'Rp ' + result : 'Rp ' + result } export const formatDate = (dateString: any) => { diff --git a/src/views/apps/ecommerce/products/detail/AddRecipeDialog.tsx b/src/views/apps/ecommerce/products/detail/AddRecipeDialog.tsx new file mode 100644 index 0000000..c0838b5 --- /dev/null +++ b/src/views/apps/ecommerce/products/detail/AddRecipeDialog.tsx @@ -0,0 +1,160 @@ +'use client' + +// MUI Imports +import Dialog from '@mui/material/Dialog' +import DialogContent from '@mui/material/DialogContent' +import DialogTitle from '@mui/material/DialogTitle' + +// Third-party Imports +import { Autocomplete, Button, Grid2, MenuItem } from '@mui/material' +import { useMemo, useState } from 'react' +import CustomTextField from '../../../../../@core/components/mui/TextField' +import DialogCloseButton from '../../../../../components/dialogs/DialogCloseButton' +import { Product } from '../../../../../types/services/product' +import { ProductRecipeRequest } from '../../../../../types/services/productRecipe' +import { useOutlets } from '../../../../../services/queries/outlets' +import { useDebounce } from 'use-debounce' +import { useIngredients } from '../../../../../services/queries/ingredients' + +// Component Imports + +type PaymentMethodProps = { + open: boolean + setOpen: (open: boolean) => void + product: Product +} + +const initialValues = { + product_id: '', + variant_id: '', + ingredient_id: '', + quantity: 0, + outlet_id: '' +} + +const AddRecipeDialog = ({ open, setOpen, product }: PaymentMethodProps) => { + const [formData, setFormData] = useState(initialValues) + + const [outletInput, setOutletInput] = useState('') + const [outletDebouncedInput] = useDebounce(outletInput, 500) + const [ingredientInput, setIngredientInput] = useState('') + const [ingredientDebouncedInput] = useDebounce(ingredientInput, 500) + + const { data: outlets, isLoading: outletsLoading } = useOutlets({ + search: outletDebouncedInput + }) + const { data: ingredients, isLoading: ingredientsLoading } = useIngredients({ + search: ingredientDebouncedInput + }) + + const outletOptions = useMemo(() => outlets?.outlets || [], [outlets]) + const ingredientOptions = useMemo(() => ingredients?.data || [], [ingredients]) + + return ( + setOpen(false)} + maxWidth='sm' + scroll='body' + closeAfterTransition={false} + sx={{ '& .MuiDialog-paper': { overflow: 'visible' } }} + > + setOpen(false)} disableRipple> + + + + Create Recipe + + + {product.variants && ( + setFormData({ ...formData, variant_id: e.target.value })} + > + {product.variants.map((variant, index) => ( + + {variant.name} + + ))} + + )} + option.name} + value={outletOptions.find(p => p.id === formData.outlet_id) || null} + onInputChange={(event, newOutlettInput) => { + setOutletInput(newOutlettInput) + }} + onChange={(event, newValue) => { + setFormData({ + ...formData, + outlet_id: newValue?.id || '' + }) + }} + renderInput={params => ( + {params.InputProps.endAdornment} + }} + /> + )} + /> + + + option.name} + value={ingredientOptions?.find(p => p.id === formData.ingredient_id) || null} + onInputChange={(event, newIngredientInput) => { + setIngredientInput(newIngredientInput) + }} + onChange={(event, newValue) => { + setFormData({ + ...formData, + ingredient_id: newValue?.id || '' + }) + }} + renderInput={params => ( + {params.InputProps.endAdornment} + }} + /> + )} + /> + + + setFormData({ ...formData, quantity: Number(e.target.value) })} + /> + + + + ) +} + +export default AddRecipeDialog diff --git a/src/views/apps/ecommerce/products/detail/AddRecipeDrawer.tsx b/src/views/apps/ecommerce/products/detail/AddRecipeDrawer.tsx new file mode 100644 index 0000000..2692c40 --- /dev/null +++ b/src/views/apps/ecommerce/products/detail/AddRecipeDrawer.tsx @@ -0,0 +1,227 @@ +// React Imports +import { useMemo, useState } from 'react' + +// MUI Imports +import Button from '@mui/material/Button' +import Divider from '@mui/material/Divider' +import Drawer from '@mui/material/Drawer' +import IconButton from '@mui/material/IconButton' +import Typography from '@mui/material/Typography' + +// Third-party Imports +import PerfectScrollbar from 'react-perfect-scrollbar' + +// Type Imports + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' +import { Autocomplete } from '@mui/material' +import { useDispatch, useSelector } from 'react-redux' +import { useDebounce } from 'use-debounce' +import { RootState } from '../../../../../redux-store' +import { resetProductRecipe } from '../../../../../redux-store/slices/productRecipe' +import { useProductRecipesMutation } from '../../../../../services/mutations/productRecipes' +import { useIngredients } from '../../../../../services/queries/ingredients' +import { useOutlets } from '../../../../../services/queries/outlets' +import { Product } from '../../../../../types/services/product' +import { ProductRecipeRequest } from '../../../../../types/services/productRecipe' + +type Props = { + open: boolean + handleClose: () => void + product: Product +} + +// Vars +const initialData = { + outlet_id: '', + product_id: '', + variant_id: '', + ingredient_id: '', + quantity: 0 +} + +const AddRecipeDrawer = (props: Props) => { + const dispatch = useDispatch() + + // Props + const { open, handleClose, product } = props + + const { currentProductRecipe } = useSelector((state: RootState) => state.productRecipeReducer) + + console.log('currentProductRecipe', currentProductRecipe) + + const [outletInput, setOutletInput] = useState('') + const [outletDebouncedInput] = useDebounce(outletInput, 500) + const [ingredientInput, setIngredientInput] = useState('') + const [ingredientDebouncedInput] = useDebounce(ingredientInput, 500) + const [formData, setFormData] = useState(initialData) + + const { data: outlets, isLoading: outletsLoading } = useOutlets({ + search: outletDebouncedInput + }) + const { data: ingredients, isLoading: ingredientsLoading } = useIngredients({ + search: ingredientDebouncedInput + }) + + const outletOptions = useMemo(() => outlets?.outlets || [], [outlets]) + const ingredientOptions = useMemo(() => ingredients?.data || [], [ingredients]) + + const { createProductRecipe, updateProductRecipe } = useProductRecipesMutation() + + const handleSubmit = (e: any) => { + e.preventDefault() + + if (currentProductRecipe.id) { + updateProductRecipe.mutate( + { id: currentProductRecipe.id, payload: formData }, + { + onSuccess: () => { + handleReset() + } + } + ) + } else { + createProductRecipe.mutate( + { ...formData, product_id: product.id, variant_id: currentProductRecipe.variant?.ID || '' }, + { + onSuccess: () => { + handleReset() + } + } + ) + } + } + + const handleReset = () => { + handleClose() + dispatch(resetProductRecipe()) + setFormData(initialData) + } + + const handleInputChange = (e: any) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }) + } + + const setTitleDrawer = (recipe: any) => { + let title = 'Original' + + if (recipe?.variant?.Name) { + title = recipe?.variant?.Name + } + + return title + } + + return ( + +
+ {setTitleDrawer(currentProductRecipe)} Variant Ingredient + + + +
+ + +
+
+ + Basic Information + + option.name} + value={outletOptions.find(p => p.id === formData.outlet_id) || null} + onInputChange={(event, newOutlettInput) => { + setOutletInput(newOutlettInput) + }} + onChange={(event, newValue) => { + setFormData({ + ...formData, + outlet_id: newValue?.id || '' + }) + }} + renderInput={params => ( + {params.InputProps.endAdornment} + }} + /> + )} + /> + option.name} + value={ingredientOptions?.find(p => p.id === formData.ingredient_id) || null} + onInputChange={(event, newIngredientInput) => { + setIngredientInput(newIngredientInput) + }} + onChange={(event, newValue) => { + setFormData({ + ...formData, + ingredient_id: newValue?.id || '' + }) + }} + renderInput={params => ( + {params.InputProps.endAdornment} + }} + /> + )} + /> + setFormData({ ...formData, quantity: Number(e.target.value) })} + /> +
+ + +
+ +
+
+
+ ) +} + +export default AddRecipeDrawer diff --git a/src/views/apps/ecommerce/products/detail/ProductDetail.tsx b/src/views/apps/ecommerce/products/detail/ProductDetail.tsx index fb5dfeb..a9efad4 100644 --- a/src/views/apps/ecommerce/products/detail/ProductDetail.tsx +++ b/src/views/apps/ecommerce/products/detail/ProductDetail.tsx @@ -2,319 +2,333 @@ import { Avatar, - Badge, + Box, + Button, Card, CardContent, - CardMedia, + CardHeader, Chip, - Divider, Grid, - List, - ListItem, - ListItemIcon, - ListItemText, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, Typography } from '@mui/material' import { useParams } from 'next/navigation' -import React, { useEffect } from 'react' +import { useState } from 'react' import { useDispatch } from 'react-redux' import Loading from '../../../../../components/layout/shared/Loading' -import { setProduct } from '../../../../../redux-store/slices/product' +import { setProductRecipe } from '../../../../../redux-store/slices/productRecipe' +import { useProductRecipesByProduct } from '../../../../../services/queries/productRecipes' import { useProductById } from '../../../../../services/queries/products' -import { ProductVariant } from '../../../../../types/services/product' -import { formatCurrency, formatDate } from '../../../../../utils/transform' -// Tabler icons (using class names) -const TablerIcon = ({ name, className = '' }: { name: string; className?: string }) => ( - -) +import { formatCurrency } from '../../../../../utils/transform' +import AddRecipeDrawer from './AddRecipeDrawer' const ProductDetail = () => { const dispatch = useDispatch() const params = useParams() + const [openProductRecipe, setOpenProductRecipe] = useState(false) + const { data: product, isLoading, error } = useProductById(params?.id as string) + const { data: productRecipe, isLoading: isLoadingProductRecipe } = useProductRecipesByProduct(params?.id as string) - useEffect(() => { - if (product) { - dispatch(setProduct(product)) + const groupedByVariant = productRecipe?.reduce((acc: any, item: any) => { + const variantId = item.variant_id + if (!acc[variantId]) { + acc[variantId] = { + variant: item.product_variant, + product: item.product, + ingredients: [] + } } - }, [product, dispatch]) + acc[variantId].ingredients.push(item) + return acc + }, {}) - const getBusinessTypeColor = (type: string) => { - switch (type.toLowerCase()) { - case 'restaurant': - return 'primary' - case 'retail': - return 'secondary' - case 'cafe': - return 'info' - default: - return 'default' - } + const handleOpenProductRecipe = (recipe: any) => { + setOpenProductRecipe(true) + dispatch(setProductRecipe(recipe)) } - const getPrinterTypeColor = (type: string) => { - switch (type.toLowerCase()) { - case 'kitchen': - return 'warning' - case 'bar': - return 'info' - case 'receipt': - return 'success' - default: - return 'default' - } - } - - const getPlainText = (html: string) => { - const doc = new DOMParser().parseFromString(html, 'text/html') - return doc.body.textContent || '' - } - - if (isLoading) return + if (isLoading || isLoadingProductRecipe) return return ( -
- {/* Header Card */} - - - - - - - -
-
-
- - {product.name} - -
- } - label={product.sku} - size='small' - variant='outlined' - /> - } - label={product.is_active ? 'Active' : 'Inactive'} - color={product.is_active ? 'success' : 'error'} - size='small' - /> -
-
-
- - {product.description && ( - - {getPlainText(product.description)} - - )} - -
-
- -
- - Price - - - {formatCurrency(product.price)} - -
-
-
- -
- - Cost - - - {formatCurrency(product.cost)} - -
-
-
- -
- } - label={product.business_type} - color={getBusinessTypeColor(product.business_type)} - size='small' - /> - } - label={product.printer_type} - color={getPrinterTypeColor(product.printer_type)} - size='small' - /> -
-
-
-
-
-
- - - {/* Product Information */} - - - - - - Product Information - -
-
- - Product ID - - - {product.id} - -
-
- - Category ID - - - {product.category_id} - -
-
- - Organization ID - - - {product.organization_id} - -
-
- - Profit Margin - - - {formatCurrency(product.price - product.cost)} - - ({(((product.price - product.cost) / product.cost) * 100).toFixed(1)}%) - - -
-
-
-
- - {/* Variants Section */} - {product.variants && product.variants.length > 0 && ( - - - - - Product Variants - + <> +
+ {/* Header Card */} + + } + title={ +
+ + {product?.name} - - {product.variants.map((variant: ProductVariant, index: number) => ( - - - - - {variant.name.charAt(0)} - - - - - {variant.name} - -
- - +{formatCurrency(variant.price_modifier)} - - - Cost: {formatCurrency(variant.cost)} - -
-
- } - secondary={ - - Total Price: {formatCurrency(product.price + variant.price_modifier)} - - } - /> - - {index < product.variants.length - 1 && } - - ))} - - -
- )} - - - {/* Metadata & Timestamps */} - - - - - - Timestamps - -
-
- - Created + +
+ } + subheader={ +
+ + SKU: {product?.sku} • Category: {product?.business_type} + +
+ + Price: {formatCurrency(product?.price || 0)} - - {formatDate(product.created_at)} - -
- -
- - Last Updated - - - {formatDate(product.updated_at)} + + Base Cost: {formatCurrency(product?.cost || 0)}
+ } + /> + - {Object.keys(product.metadata).length > 0 && ( - <> - - - Metadata - -
- {Object.entries(product.metadata).map(([key, value]) => ( -
- - {key.replace(/_/g, ' ')} - - - {typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)} - + {productRecipe && ( +
+ {/* Recipe Details by Variant */} +
+
+ + + Recipe Details + +
+ + {Object.keys(groupedByVariant).length > 0 ? ( + Object.entries(groupedByVariant).map(([variantId, variantData]: any) => ( + + +
+ + + {variantData?.variant?.Name || 'Original'} Variant + +
+
+ + +
+
+ } + /> + + + + + + +
+ + Ingredient +
+
+ +
+ + Quantity +
+
+ +
+ + Unit Cost +
+
+ +
+ + Stock Available +
+
+ +
+ + Total Cost +
+
+
+
+ + {variantData.ingredients.map((item: any) => ( + + +
+
+
+ + {item.ingredient.name} + + + {item.ingredient.is_semi_finished ? 'Semi-finished' : 'Raw ingredient'} + +
+
+ + + + + {formatCurrency(item.ingredient.cost)} + + 5 ? 'success' : 'warning'} + variant='outlined' + /> + + + {formatCurrency(item.ingredient.cost * item.quantity)} + + + ))} + +
+
+ + {/* Variant Summary */} + + + + + + Total Ingredients: + {variantData.ingredients.length} + + + + + + Total Recipe Cost: + {formatCurrency( + variantData.ingredients.reduce( + (sum: any, item: any) => sum + item.ingredient.cost * item.quantity, + 0 + ) + )} + + + + + + +
+ + )) + ) : ( + + +
+ + + Original Variant + +
+
+ + +
- ))} -
- + } + /> + + + + + + +
+ + Ingredient +
+
+ +
+ + Quantity +
+
+ +
+ + Unit Cost +
+
+ +
+ + Stock Available +
+
+ +
+ + Total Cost +
+
+
+
+ +
+
+ + +
+ )} - - - - -
+
+
+ )} +
+ + setOpenProductRecipe(false)} product={product!} /> + ) } diff --git a/src/views/dashboards/profit-loss/EarningReportWithTabs.tsx b/src/views/dashboards/profit-loss/EarningReportWithTabs.tsx index edc9373..fb213f8 100644 --- a/src/views/dashboards/profit-loss/EarningReportWithTabs.tsx +++ b/src/views/dashboards/profit-loss/EarningReportWithTabs.tsx @@ -9,7 +9,6 @@ import dynamic from 'next/dynamic' // MUI Imports import TabContext from '@mui/lab/TabContext' -import TabList from '@mui/lab/TabList' import TabPanel from '@mui/lab/TabPanel' import Card from '@mui/material/Card' import CardContent from '@mui/material/CardContent' @@ -26,7 +25,6 @@ import classnames from 'classnames' // Components Imports import CustomAvatar from '@core/components/mui/Avatar' import OptionMenu from '@core/components/option-menu' -import Loading from '../../../components/layout/shared/Loading' import { formatShortCurrency } from '../../../utils/transform' // Styled Component Imports @@ -207,38 +205,12 @@ const MultipleSeries = ({ data }: { data: TabType[] }) => { return ( } /> - {data.length > 1 && ( - - {renderTabs(data, value)} - - - - - - } - /> - - )} {renderTabPanels(data, theme, options, colors)}