feat: active & deactivate the price for product

This commit is contained in:
2026-05-14 16:20:59 +02:00
parent 0fe8a06bfa
commit e0c354d76c
4 changed files with 66 additions and 15 deletions
+4 -4
View File
@@ -69,8 +69,8 @@ func (d *Database) CreateProduct(product any) error {
p.SetUpdatedAt(result.UpdatedAt) p.SetUpdatedAt(result.UpdatedAt)
for i, price := range p.GetPrices() { for i, price := range p.GetPrices() {
err := d.GDB.Exec(`INSERT INTO product_prices (product_id, quantity, price) VALUES (?, ?, ?)`, err := d.GDB.Exec(`INSERT INTO product_prices (product_id, quantity, price, active_price) VALUES (?, ?, ?, ?)`,
result.ID, price.Quantity, price.Price).Error result.ID, price.Quantity, price.Price, price.ActivePrice).Error
if err != nil { if err != nil {
log.Printf("❌ [DB CreateProduct] Erreur insertion prix[%d]: %v", i, err) log.Printf("❌ [DB CreateProduct] Erreur insertion prix[%d]: %v", i, err)
return fmt.Errorf("erreur insertion prix: %v", 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) d.GDB.Exec(`DELETE FROM product_prices WHERE product_id = ?`, productID)
for _, price := range prices { for _, price := range prices {
if err := d.GDB.Exec(`INSERT INTO product_prices (product_id, quantity, price) VALUES (?, ?, ?)`, if err := d.GDB.Exec(`INSERT INTO product_prices (product_id, quantity, price, active_price) VALUES (?, ?, ?, ?)`,
productID, price.Quantity, price.Price).Error; err != nil { productID, price.Quantity, price.Price, price.ActivePrice).Error; err != nil {
log.Printf("❌ [UpdateProduct] Erreur prix: %v", err) log.Printf("❌ [UpdateProduct] Erreur prix: %v", err)
} }
} }
+14
View File
@@ -747,6 +747,20 @@ export const deleteProductMediaAdmin = async (
return { success: true, message: data.message }; 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 // ADDRESSES
// ============================================ // ============================================
+1 -1
View File
@@ -102,7 +102,7 @@ export interface Product {
category: string; category: string;
stock: number; stock: number;
unit: string; 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 }>; media?: Array<{ url: string; type: string; id?: number; created_at?: string }>;
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
@@ -26,6 +26,8 @@ import {
deleteProductAdmin, deleteProductAdmin,
uploadProductMediaAdmin, uploadProductMediaAdmin,
deleteProductMediaAdmin, deleteProductMediaAdmin,
activateProductPrice,
deactivateProductPrice,
getCategories, getCategories,
} from "../../api/api_admin"; } from "../../api/api_admin";
import type { Category } from "../../api/api_admin"; import type { Category } from "../../api/api_admin";
@@ -40,8 +42,10 @@ import { useAlert } from "../../hooks/useAlert";
// Types // Types
// -------------------------------------------------- // --------------------------------------------------
interface PriceRow { interface PriceRow {
id?: number;
quantity: string; quantity: string;
price: string; price: string;
active: boolean;
} }
interface MediaItem { interface MediaItem {
id?: number; id?: number;
@@ -82,7 +86,7 @@ const emptyForm = (firstCategory = ""): FormState => ({
description: "", description: "",
stock: "", stock: "",
unit: "u", unit: "u",
prices: [{ quantity: "1", price: "" }], prices: [{ quantity: "1", price: "", active: true }],
}); });
// ================================================== // ==================================================
@@ -162,10 +166,12 @@ export default function ProductsScreen() {
prices: prices:
product.prices && product.prices.length > 0 product.prices && product.prices.length > 0
? product.prices.map((p) => ({ ? product.prices.map((p) => ({
id: p.id,
quantity: p.quantity.toString(), quantity: p.quantity.toString(),
price: p.price.toString(), price: p.price.toString(),
active: p.active_price ?? true,
})) }))
: [{ quantity: "1", price: "" }], : [{ quantity: "1", price: "", active: true }],
}); });
setExistingMedia( setExistingMedia(
(product.media || []).map((m) => ({ (product.media || []).map((m) => ({
@@ -191,8 +197,15 @@ export default function ProductsScreen() {
const addPriceRow = () => const addPriceRow = () =>
setForm((f) => ({ setForm((f) => ({
...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) => const removePriceRow = (idx: number) =>
setForm((f) => ({ setForm((f) => ({
...f, ...f,
@@ -292,6 +305,7 @@ export default function ProductsScreen() {
const prices = form.prices.map((p) => ({ const prices = form.prices.map((p) => ({
quantity: parseFloat(p.quantity), quantity: parseFloat(p.quantity),
price: parseFloat(p.price), price: parseFloat(p.price),
active_price: p.active,
})); }));
try { try {
@@ -329,11 +343,10 @@ export default function ProductsScreen() {
fd.append("stock", form.stock); fd.append("stock", form.stock);
fd.append("unit", form.unit); 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) => { prices.forEach((p, i) => {
fd.append(`prices[${i}][quantity]`, String(p.quantity)); fd.append(`prices[${i}][quantity]`, String(p.quantity));
fd.append(`prices[${i}][price]`, String(p.price)); 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 // Attacher les médias en attente
@@ -647,6 +660,7 @@ export default function ProductsScreen() {
paddingHorizontal: spacing.m, paddingHorizontal: spacing.m,
}, },
removePriceBtn: { padding: spacing.s, marginBottom: 4 }, removePriceBtn: { padding: spacing.s, marginBottom: 4 },
toggleActiveBtn: { padding: spacing.s, marginBottom: 4 },
// Media section // Media section
mediaSectionBox: { marginTop: spacing.m }, mediaSectionBox: { marginTop: spacing.m },
@@ -803,11 +817,24 @@ export default function ProductsScreen() {
)} )}
<Text style={styles.info}>Stock: {item.stock} {item.unit || "u"}</Text> <Text style={styles.info}>Stock: {item.stock} {item.unit || "u"}</Text>
{item.prices && item.prices.length > 0 && ( {item.prices && item.prices.length > 0 && (
<Text style={styles.info}> <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 4, marginTop: 2 }}>
{item.prices {item.prices.map((p, i) => (
.map((p) => `${p.quantity}${item.unit || "u"} = ${p.price}`) <Text
.join(" | ")} 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> </Text>
))}
</View>
)} )}
{item.description && ( {item.description && (
<Text style={styles.desc} numberOfLines={2}> <Text style={styles.desc} numberOfLines={2}>
@@ -1055,6 +1082,16 @@ export default function ProductsScreen() {
} }
/> />
</View> </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 && ( {form.prices.length > 1 && (
<TouchableOpacity <TouchableOpacity
style={styles.removePriceBtn} style={styles.removePriceBtn}