feat: active & deactivate the price for product
This commit is contained in:
@@ -69,8 +69,8 @@ func (d *Database) CreateProduct(product any) error {
|
||||
p.SetUpdatedAt(result.UpdatedAt)
|
||||
|
||||
for i, price := range p.GetPrices() {
|
||||
err := d.GDB.Exec(`INSERT INTO product_prices (product_id, quantity, price) VALUES (?, ?, ?)`,
|
||||
result.ID, price.Quantity, price.Price).Error
|
||||
err := d.GDB.Exec(`INSERT INTO product_prices (product_id, quantity, price, active_price) VALUES (?, ?, ?, ?)`,
|
||||
result.ID, price.Quantity, price.Price, price.ActivePrice).Error
|
||||
if err != nil {
|
||||
log.Printf("❌ [DB CreateProduct] Erreur insertion prix[%d]: %v", i, err)
|
||||
return fmt.Errorf("erreur insertion prix: %v", err)
|
||||
@@ -190,8 +190,8 @@ func (d *Database) UpdateProduct(productID int, name, category, description, uni
|
||||
d.GDB.Exec(`DELETE FROM product_prices WHERE product_id = ?`, productID)
|
||||
|
||||
for _, price := range prices {
|
||||
if err := d.GDB.Exec(`INSERT INTO product_prices (product_id, quantity, price) VALUES (?, ?, ?)`,
|
||||
productID, price.Quantity, price.Price).Error; err != nil {
|
||||
if err := d.GDB.Exec(`INSERT INTO product_prices (product_id, quantity, price, active_price) VALUES (?, ?, ?, ?)`,
|
||||
productID, price.Quantity, price.Price, price.ActivePrice).Error; err != nil {
|
||||
log.Printf("❌ [UpdateProduct] Erreur prix: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -747,6 +747,20 @@ export const deleteProductMediaAdmin = async (
|
||||
return { success: true, message: data.message };
|
||||
};
|
||||
|
||||
export const activateProductPrice = async (priceId: number) => {
|
||||
const { data } = await apiClient.post(
|
||||
`${V2}/admin/protected/active/product/price/${priceId}`,
|
||||
);
|
||||
return { success: true, message: data.message };
|
||||
};
|
||||
|
||||
export const deactivateProductPrice = async (priceId: number) => {
|
||||
const { data } = await apiClient.post(
|
||||
`${V2}/admin/protected/desactive/product/price/${priceId}`,
|
||||
);
|
||||
return { success: true, message: data.message };
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// ADDRESSES
|
||||
// ============================================
|
||||
|
||||
@@ -102,7 +102,7 @@ export interface Product {
|
||||
category: string;
|
||||
stock: number;
|
||||
unit: string;
|
||||
prices?: Array<{ quantity: number; price: number }>;
|
||||
prices?: Array<{ id?: number; quantity: number; price: number; active_price?: boolean }>;
|
||||
media?: Array<{ url: string; type: string; id?: number; created_at?: string }>;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
deleteProductAdmin,
|
||||
uploadProductMediaAdmin,
|
||||
deleteProductMediaAdmin,
|
||||
activateProductPrice,
|
||||
deactivateProductPrice,
|
||||
getCategories,
|
||||
} from "../../api/api_admin";
|
||||
import type { Category } from "../../api/api_admin";
|
||||
@@ -40,8 +42,10 @@ import { useAlert } from "../../hooks/useAlert";
|
||||
// Types
|
||||
// --------------------------------------------------
|
||||
interface PriceRow {
|
||||
id?: number;
|
||||
quantity: string;
|
||||
price: string;
|
||||
active: boolean;
|
||||
}
|
||||
interface MediaItem {
|
||||
id?: number;
|
||||
@@ -82,7 +86,7 @@ const emptyForm = (firstCategory = ""): FormState => ({
|
||||
description: "",
|
||||
stock: "",
|
||||
unit: "u",
|
||||
prices: [{ quantity: "1", price: "" }],
|
||||
prices: [{ quantity: "1", price: "", active: true }],
|
||||
});
|
||||
|
||||
// ==================================================
|
||||
@@ -162,10 +166,12 @@ export default function ProductsScreen() {
|
||||
prices:
|
||||
product.prices && product.prices.length > 0
|
||||
? product.prices.map((p) => ({
|
||||
id: p.id,
|
||||
quantity: p.quantity.toString(),
|
||||
price: p.price.toString(),
|
||||
active: p.active_price ?? true,
|
||||
}))
|
||||
: [{ quantity: "1", price: "" }],
|
||||
: [{ quantity: "1", price: "", active: true }],
|
||||
});
|
||||
setExistingMedia(
|
||||
(product.media || []).map((m) => ({
|
||||
@@ -191,8 +197,15 @@ export default function ProductsScreen() {
|
||||
const addPriceRow = () =>
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
prices: [...f.prices, { quantity: "1", price: "" }],
|
||||
prices: [...f.prices, { quantity: "1", price: "", active: true }],
|
||||
}));
|
||||
|
||||
const togglePriceActive = (idx: number) =>
|
||||
setForm((f) => {
|
||||
const prices = [...f.prices];
|
||||
prices[idx] = { ...prices[idx], active: !prices[idx].active };
|
||||
return { ...f, prices };
|
||||
});
|
||||
const removePriceRow = (idx: number) =>
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
@@ -292,6 +305,7 @@ export default function ProductsScreen() {
|
||||
const prices = form.prices.map((p) => ({
|
||||
quantity: parseFloat(p.quantity),
|
||||
price: parseFloat(p.price),
|
||||
active_price: p.active,
|
||||
}));
|
||||
|
||||
try {
|
||||
@@ -329,11 +343,10 @@ export default function ProductsScreen() {
|
||||
fd.append("stock", form.stock);
|
||||
fd.append("unit", form.unit);
|
||||
|
||||
// ✅ FIX PRINCIPAL : Envoyer les prix au format que Go attend
|
||||
// Backend attend : prices[0][quantity], prices[0][price], etc.
|
||||
prices.forEach((p, i) => {
|
||||
fd.append(`prices[${i}][quantity]`, String(p.quantity));
|
||||
fd.append(`prices[${i}][price]`, String(p.price));
|
||||
fd.append(`prices[${i}][active_price]`, p.active_price ? "true" : "false");
|
||||
});
|
||||
|
||||
// Attacher les médias en attente
|
||||
@@ -647,6 +660,7 @@ export default function ProductsScreen() {
|
||||
paddingHorizontal: spacing.m,
|
||||
},
|
||||
removePriceBtn: { padding: spacing.s, marginBottom: 4 },
|
||||
toggleActiveBtn: { padding: spacing.s, marginBottom: 4 },
|
||||
|
||||
// Media section
|
||||
mediaSectionBox: { marginTop: spacing.m },
|
||||
@@ -803,11 +817,24 @@ export default function ProductsScreen() {
|
||||
)}
|
||||
<Text style={styles.info}>Stock: {item.stock} {item.unit || "u"}</Text>
|
||||
{item.prices && item.prices.length > 0 && (
|
||||
<Text style={styles.info}>
|
||||
{item.prices
|
||||
.map((p) => `${p.quantity}${item.unit || "u"} = ${p.price}€`)
|
||||
.join(" | ")}
|
||||
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 4, marginTop: 2 }}>
|
||||
{item.prices.map((p, i) => (
|
||||
<Text
|
||||
key={i}
|
||||
style={[
|
||||
styles.info,
|
||||
!p.active_price && {
|
||||
textDecorationLine: "line-through",
|
||||
color: colors.textMuted,
|
||||
opacity: 0.5,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{p.quantity}{item.unit || "u"} = {p.price}€
|
||||
{i < item.prices!.length - 1 ? " |" : ""}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
{item.description && (
|
||||
<Text style={styles.desc} numberOfLines={2}>
|
||||
@@ -1055,6 +1082,16 @@ export default function ProductsScreen() {
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.toggleActiveBtn}
|
||||
onPress={() => togglePriceActive(idx)}
|
||||
>
|
||||
<Ionicons
|
||||
name={p.active ? "checkmark-circle" : "close-circle"}
|
||||
size={22}
|
||||
color={p.active ? colors.success || "#22c55e" : colors.danger}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
{form.prices.length > 1 && (
|
||||
<TouchableOpacity
|
||||
style={styles.removePriceBtn}
|
||||
|
||||
Reference in New Issue
Block a user