From dd31473ca182a0b8488f3a40e61145210d4b94e7 Mon Sep 17 00:00:00 2001 From: Xor290 Date: Sat, 7 Mar 2026 15:10:18 +0100 Subject: [PATCH] chore: features category --- frontend-admin/eas.json | 6 +- frontend-admin/src/api/api_admin.ts | 74 ++++- frontend-admin/src/api/api_cabine.ts | 4 +- frontend-admin/src/api/api_delivery.ts | 2 +- frontend-admin/src/api/client.ts | 2 +- .../src/navigation/AdminNavigator.tsx | 15 + frontend-admin/src/navigation/types.ts | 1 + .../src/screens/admin/CategoriesScreen.tsx | 280 ++++++++++++++++++ .../src/screens/admin/ProductsScreen.tsx | 33 ++- mobile/eas.json | 6 +- mobile/src/api/api.ts | 17 +- mobile/src/api/client.ts | 2 +- mobile/src/screens/client/ProductsScreen.tsx | 53 ++-- 13 files changed, 433 insertions(+), 62 deletions(-) create mode 100644 frontend-admin/src/screens/admin/CategoriesScreen.tsx diff --git a/frontend-admin/eas.json b/frontend-admin/eas.json index 150b0152..c3b769bf 100644 --- a/frontend-admin/eas.json +++ b/frontend-admin/eas.json @@ -15,7 +15,7 @@ "simulator": true }, "env": { - "API_URL": "https://uber-stup.club" + "API_URL": "http://5.181.0.112" } }, "preview": { @@ -24,7 +24,7 @@ "buildType": "apk" }, "env": { - "API_URL": "https://uber-stup.club" + "API_URL": "http://5.181.0.112" } }, "production": { @@ -34,7 +34,7 @@ "buildType": "apk" }, "env": { - "API_URL": "https://uber-stup.club" + "API_URL": "http://5.181.0.112" } } } diff --git a/frontend-admin/src/api/api_admin.ts b/frontend-admin/src/api/api_admin.ts index 3393db6f..5461cb7e 100644 --- a/frontend-admin/src/api/api_admin.ts +++ b/frontend-admin/src/api/api_admin.ts @@ -9,8 +9,8 @@ import type { Alert, } from "./types"; -const V2 = "https://uber-stup.club/api/v2"; -const CABINE_URL = "https://uber-stup.club/api/v1/cabine"; +const V2 = "http://5.181.0.112/api/v2"; +const CABINE_URL = "http://5.181.0.112/api/v1/cabine"; // ============================================ // AUTH @@ -580,3 +580,73 @@ export const getCommandCountByStatus = async ( return 0; } }; + +// ============================================ +// CATÉGORIES +// ============================================ + +const V1_PUBLIC = "https://uber-stup.club/api/v1"; + +export interface Category { + id: number; + name: string; + created_at: string; +} + +export const getCategories = async (): Promise => { + try { + const { data } = await apiClient.get(`${V1_PUBLIC}/categories`); + return data.categories || []; + } catch { + return []; + } +}; + +export const createCategoryAdmin = async ( + name: string, +): Promise<{ success: boolean; category?: Category; error?: string }> => { + try { + const { data } = await apiClient.post( + `${V2}/admin/protected/categories`, + { name }, + ); + return { success: true, category: data.category }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.error || "Erreur", + }; + } +}; + +export const updateCategoryAdmin = async ( + id: number, + name: string, +): Promise<{ success: boolean; category?: Category; error?: string }> => { + try { + const { data } = await apiClient.put( + `${V2}/admin/protected/categories/${id}`, + { name }, + ); + return { success: true, category: data.category }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.error || "Erreur", + }; + } +}; + +export const deleteCategoryAdmin = async ( + id: number, +): Promise<{ success: boolean; error?: string }> => { + try { + await apiClient.delete(`${V2}/admin/protected/categories/${id}`); + return { success: true }; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.error || "Erreur", + }; + } +}; diff --git a/frontend-admin/src/api/api_cabine.ts b/frontend-admin/src/api/api_cabine.ts index a8fa014c..6d776f50 100644 --- a/frontend-admin/src/api/api_cabine.ts +++ b/frontend-admin/src/api/api_cabine.ts @@ -6,8 +6,8 @@ import type { Alert, } from "./types"; -const API = "https://uber-stup.club/api/v1/cabine"; -const V2 = "https://uber-stup.club/api/v2"; +const API = "http://5.181.0.112/api/v1/cabine"; +const V2 = "http://5.181.0.112/api/v2"; // ============================================ // ITEMS diff --git a/frontend-admin/src/api/api_delivery.ts b/frontend-admin/src/api/api_delivery.ts index 4ff46292..dcea78a7 100644 --- a/frontend-admin/src/api/api_delivery.ts +++ b/frontend-admin/src/api/api_delivery.ts @@ -7,7 +7,7 @@ import type { Alert, } from "./types"; -const API = "https://uber-stup.club/api/v1/livreur"; +const API = "http://5.181.0.112/api/v1/livreur"; // ============================================ // STATUT diff --git a/frontend-admin/src/api/client.ts b/frontend-admin/src/api/client.ts index 4fe0135d..4961112e 100644 --- a/frontend-admin/src/api/client.ts +++ b/frontend-admin/src/api/client.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { getToken, getAdminToken } from "../auth/tokenStorage"; // Change this to your server IP/domain -export const API_BASE_URL = "https://uber-stup.club"; +export const API_BASE_URL = "http://5.181.0.112"; const apiClient = axios.create({ baseURL: API_BASE_URL, diff --git a/frontend-admin/src/navigation/AdminNavigator.tsx b/frontend-admin/src/navigation/AdminNavigator.tsx index f8f10e77..013f9b16 100644 --- a/frontend-admin/src/navigation/AdminNavigator.tsx +++ b/frontend-admin/src/navigation/AdminNavigator.tsx @@ -14,6 +14,7 @@ import OrdersScreen from "../screens/admin/OrdersScreen"; import OrderDetailScreen from "../screens/admin/OrderDetailScreen"; import UsersScreen from "../screens/admin/UsersScreen"; import ProductsScreen from "../screens/admin/ProductsScreen"; +import CategoriesScreen from "../screens/admin/CategoriesScreen"; import DeliveryScreen from "../screens/admin/DeliveryScreen"; import AlertsScreen from "../screens/admin/AlertsScreen"; import AddressScreen from "../screens/admin/AddressScreen"; @@ -128,6 +129,20 @@ function AdminTabs() { ), }} /> + ( + + ), + }} + /> ([]); + const [loading, setLoading] = useState(true); + const [modalVisible, setModalVisible] = useState(false); + const [editingCategory, setEditingCategory] = useState(null); + const [name, setName] = useState(""); + const [saving, setSaving] = useState(false); + const { alert, showError, showSuccess, showConfirm, hideAlert } = useAlert(); + + const load = useCallback(async () => { + setLoading(true); + const data = await getCategories(); + setCategories(data); + setLoading(false); + }, []); + + useEffect(() => { + load(); + }, [load]); + + const openCreate = () => { + setEditingCategory(null); + setName(""); + setModalVisible(true); + }; + + const openEdit = (cat: Category) => { + setEditingCategory(cat); + setName(cat.name); + setModalVisible(true); + }; + + const closeModal = () => { + setModalVisible(false); + setName(""); + setEditingCategory(null); + }; + + const handleSave = async () => { + const trimmed = name.trim(); + if (!trimmed) { + showError("Erreur", "Le nom est requis"); + return; + } + setSaving(true); + if (editingCategory) { + const res = await updateCategoryAdmin(editingCategory.id, trimmed); + if (res.success) { + showSuccess("Succès", "Catégorie modifiée"); + closeModal(); + load(); + } else { + showError("Erreur", res.error || "Erreur"); + } + } else { + const res = await createCategoryAdmin(trimmed); + if (res.success) { + showSuccess("Succès", "Catégorie créée"); + closeModal(); + load(); + } else { + showError("Erreur", res.error || "Erreur"); + } + } + setSaving(false); + }; + + const handleDelete = (cat: Category) => { + showConfirm( + "Supprimer", + `Supprimer la catégorie "${cat.name}" ?`, + async () => { + const res = await deleteCategoryAdmin(cat.id); + if (res.success) { + showSuccess("Succès", "Catégorie supprimée"); + load(); + } else { + showError("Erreur", res.error || "Erreur"); + } + }, + ); + }; + + const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: colors.background }, + header: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + padding: spacing.m, + borderBottomWidth: 1, + borderBottomColor: colors.border, + }, + title: { fontSize: fontSize.xl, fontWeight: "700", color: colors.text }, + addBtn: { + flexDirection: "row", + alignItems: "center", + gap: 6, + backgroundColor: colors.accent, + paddingHorizontal: spacing.m, + paddingVertical: spacing.s, + borderRadius: borderRadius.m, + }, + addBtnText: { color: "#fff", fontWeight: "600", fontSize: fontSize.s }, + list: { padding: spacing.m, gap: spacing.s }, + row: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, + catName: { fontSize: fontSize.m, color: colors.text, fontWeight: "600", textTransform: "capitalize" }, + catDate: { fontSize: fontSize.xs, color: colors.textSecondary, marginTop: 2 }, + actions: { flexDirection: "row", gap: spacing.s }, + actionBtn: { padding: 8 }, + overlay: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.5)", + justifyContent: "center", + padding: spacing.l, + }, + modal: { + backgroundColor: colors.surface, + borderRadius: borderRadius.l, + padding: spacing.l, + gap: spacing.m, + }, + modalTitle: { fontSize: fontSize.l, fontWeight: "700", color: colors.text }, + input: { + borderWidth: 1, + borderColor: colors.border, + borderRadius: borderRadius.m, + padding: spacing.m, + color: colors.text, + fontSize: fontSize.m, + backgroundColor: colors.background, + }, + modalBtns: { flexDirection: "row", gap: spacing.s }, + cancelBtn: { + flex: 1, + padding: spacing.m, + borderRadius: borderRadius.m, + borderWidth: 1, + borderColor: colors.border, + alignItems: "center", + }, + cancelBtnText: { color: colors.text, fontWeight: "600" }, + saveBtn: { + flex: 1, + padding: spacing.m, + borderRadius: borderRadius.m, + backgroundColor: colors.accent, + alignItems: "center", + }, + saveBtnText: { color: "#fff", fontWeight: "600" }, + empty: { textAlign: "center", color: colors.textSecondary, marginTop: spacing.xl }, + }); + + if (loading) return ; + + return ( + + + Catégories + + + Ajouter + + + + String(item.id)} + contentContainerStyle={styles.list} + ListEmptyComponent={ + Aucune catégorie. Créez-en une ! + } + renderItem={({ item }) => ( + + + + {item.name} + + Créée le {new Date(item.created_at).toLocaleDateString("fr-FR")} + + + + openEdit(item)} + > + + + handleDelete(item)} + > + + + + + + )} + /> + + + + + + {editingCategory ? "Modifier la catégorie" : "Nouvelle catégorie"} + + + + + Annuler + + + + {saving ? "..." : "Enregistrer"} + + + + + + + + + + ); +} diff --git a/frontend-admin/src/screens/admin/ProductsScreen.tsx b/frontend-admin/src/screens/admin/ProductsScreen.tsx index dbeef734..3a5ccb1b 100644 --- a/frontend-admin/src/screens/admin/ProductsScreen.tsx +++ b/frontend-admin/src/screens/admin/ProductsScreen.tsx @@ -25,7 +25,9 @@ import { deleteProductAdmin, uploadProductMediaAdmin, deleteProductMediaAdmin, + getCategories, } from "../../api/api_admin"; +import type { Category } from "../../api/api_admin"; import type { Product } from "../../api/types"; import LoadingSpinner from "../../components/ui/LoadingSpinner"; import Card from "../../components/ui/Card"; @@ -54,11 +56,7 @@ interface PendingMedia { mediaType: "image" | "video"; } -const CATEGORIES = [ - { value: "weed&hash", label: "Weed & Hash" }, - { value: "zipette&co", label: "Zipette & Co" }, - { value: "gros&semi", label: "Gros & Semi" }, -]; +// Les catégories sont chargées dynamiquement depuis l'API const UNITS = [ { value: "u", label: "u" }, @@ -79,9 +77,9 @@ interface FormState { prices: PriceRow[]; } -const emptyForm = (): FormState => ({ +const emptyForm = (firstCategory = ""): FormState => ({ name: "", - category: "weed&hash", + category: firstCategory, description: "", stock: "", unit: "u", @@ -94,6 +92,7 @@ const emptyForm = (): FormState => ({ export default function ProductsScreen() { const { colors } = useTheme(); const [products, setProducts] = useState([]); + const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); @@ -117,8 +116,12 @@ export default function ProductsScreen() { // -------------------------------------------------- const loadData = useCallback(async () => { try { - const result = await getAllProductsAdmin(); + const [result, cats] = await Promise.all([ + getAllProductsAdmin(), + getCategories(), + ]); setProducts(result.data); + setCategories(cats); } catch { /* ignore */ } @@ -139,7 +142,7 @@ export default function ProductsScreen() { // -------------------------------------------------- const openCreate = () => { setEditingProduct(null); - setForm(emptyForm()); + setForm(emptyForm(categories[0]?.name || "")); setExistingMedia([]); setMediaToDelete([]); setPendingMedia([]); @@ -896,29 +899,29 @@ export default function ProductsScreen() { {/* Catégorie */} Catégorie * - {CATEGORIES.map((c) => ( + {categories.map((c) => ( setForm((f) => ({ ...f, - category: c.value, + category: c.name, })) } > - {c.label} + {c.name} ))} diff --git a/mobile/eas.json b/mobile/eas.json index 86bb7597..24c47967 100644 --- a/mobile/eas.json +++ b/mobile/eas.json @@ -15,7 +15,7 @@ "simulator": true }, "env": { - "API_URL": "https://uber-stup.club" + "API_URL": "http://5.181.0.112" }, "channel": "development" }, @@ -25,7 +25,7 @@ "buildType": "apk" }, "env": { - "API_URL": "https://uber-stup.club" + "API_URL": "http://5.181.0.112" }, "channel": "preview" }, @@ -36,7 +36,7 @@ "buildType": "apk" }, "env": { - "API_URL": "https://uber-stup.club" + "API_URL": "http://5.181.0.112" }, "channel": "production" } diff --git a/mobile/src/api/api.ts b/mobile/src/api/api.ts index d360f6c8..53f6a3c2 100644 --- a/mobile/src/api/api.ts +++ b/mobile/src/api/api.ts @@ -11,7 +11,7 @@ import type { import { getToken } from "../auth/tokenStorage"; import { extractUsernameFromToken } from "../auth/jwtUtils"; -const V1 = "https://uber-stup.club/api/v1"; +const V1 = "http://5.181.0.112/api/v1"; export const getJwtUsername = async (): Promise => { const token = await getToken(); @@ -113,6 +113,21 @@ export const logoutUser = async (): Promise => { // PRODUCTS // ============================================ +export interface Category { + id: number; + name: string; + created_at: string; +} + +export const getCategories = async (): Promise => { + try { + const { data } = await apiClient.get(`${V1}/categories`); + return data.categories || []; + } catch { + return []; + } +}; + export const getAllProducts = async () => { try { const { data } = await apiClient.get(`${V1}/products`); diff --git a/mobile/src/api/client.ts b/mobile/src/api/client.ts index d75a799d..922c5c8f 100644 --- a/mobile/src/api/client.ts +++ b/mobile/src/api/client.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { getToken, getAdminToken } from "../auth/tokenStorage"; // Change this to your server IP/domain -export const API_BASE_URL = "https://uber-stup.club"; +export const API_BASE_URL = "http://5.181.0.112"; const apiClient = axios.create({ baseURL: API_BASE_URL, diff --git a/mobile/src/screens/client/ProductsScreen.tsx b/mobile/src/screens/client/ProductsScreen.tsx index 7845f39a..c17bee12 100644 --- a/mobile/src/screens/client/ProductsScreen.tsx +++ b/mobile/src/screens/client/ProductsScreen.tsx @@ -18,7 +18,8 @@ import { } from "react-native"; import { useNavigation } from "@react-navigation/native"; import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; -import { getAllProducts, getProductsByCategory } from "../../api/api"; +import { getAllProducts, getProductsByCategory, getCategories } from "../../api/api"; +import type { Category } from "../../api/api"; import type { Product } from "../../api/api_types"; import ProductCard from "../../components/ProductCard"; import CategoryPill from "../../components/CategoryPill"; @@ -33,25 +34,14 @@ const logoGrosSemi = require("../../../assets/logo-gros-semi.png"); const { width: SCREEN_WIDTH } = Dimensions.get("window"); const CARD_WIDTH = SCREEN_WIDTH - 48; -const CATEGORIES = [ - { label: "Tous", value: "tous" }, - { label: "Weed&Hash", value: "weed&hash" }, - { label: "Zipette&Co", value: "zipette&co" }, - { label: "Gros&Semi", value: "gros&semi" }, -]; - -const CATEGORY_TITLES: Record = { - tous: "Tous les produits", - "weed&hash": "Weed & Hash", - "zipette&co": "Zipette & Co", - "gros&semi": "Gros & Semi", -}; +// Les catégories sont chargées dynamiquement depuis l'API type Nav = NativeStackNavigationProp; export default function ProductsScreen() { const { colors } = useTheme(); const navigation = useNavigation