chore: features category
This commit is contained in:
@@ -15,7 +15,7 @@
|
|||||||
"simulator": true
|
"simulator": true
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"API_URL": "https://uber-stup.club"
|
"API_URL": "http://5.181.0.112"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"buildType": "apk"
|
"buildType": "apk"
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"API_URL": "https://uber-stup.club"
|
"API_URL": "http://5.181.0.112"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"buildType": "apk"
|
"buildType": "apk"
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"API_URL": "https://uber-stup.club"
|
"API_URL": "http://5.181.0.112"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import type {
|
|||||||
Alert,
|
Alert,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const V2 = "https://uber-stup.club/api/v2";
|
const V2 = "http://5.181.0.112/api/v2";
|
||||||
const CABINE_URL = "https://uber-stup.club/api/v1/cabine";
|
const CABINE_URL = "http://5.181.0.112/api/v1/cabine";
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// AUTH
|
// AUTH
|
||||||
@@ -580,3 +580,73 @@ export const getCommandCountByStatus = async (
|
|||||||
return 0;
|
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<Category[]> => {
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import type {
|
|||||||
Alert,
|
Alert,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const API = "https://uber-stup.club/api/v1/cabine";
|
const API = "http://5.181.0.112/api/v1/cabine";
|
||||||
const V2 = "https://uber-stup.club/api/v2";
|
const V2 = "http://5.181.0.112/api/v2";
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// ITEMS
|
// ITEMS
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type {
|
|||||||
Alert,
|
Alert,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const API = "https://uber-stup.club/api/v1/livreur";
|
const API = "http://5.181.0.112/api/v1/livreur";
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// STATUT
|
// STATUT
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import axios from "axios";
|
|||||||
import { getToken, getAdminToken } from "../auth/tokenStorage";
|
import { getToken, getAdminToken } from "../auth/tokenStorage";
|
||||||
|
|
||||||
// Change this to your server IP/domain
|
// 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({
|
const apiClient = axios.create({
|
||||||
baseURL: API_BASE_URL,
|
baseURL: API_BASE_URL,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import OrdersScreen from "../screens/admin/OrdersScreen";
|
|||||||
import OrderDetailScreen from "../screens/admin/OrderDetailScreen";
|
import OrderDetailScreen from "../screens/admin/OrderDetailScreen";
|
||||||
import UsersScreen from "../screens/admin/UsersScreen";
|
import UsersScreen from "../screens/admin/UsersScreen";
|
||||||
import ProductsScreen from "../screens/admin/ProductsScreen";
|
import ProductsScreen from "../screens/admin/ProductsScreen";
|
||||||
|
import CategoriesScreen from "../screens/admin/CategoriesScreen";
|
||||||
import DeliveryScreen from "../screens/admin/DeliveryScreen";
|
import DeliveryScreen from "../screens/admin/DeliveryScreen";
|
||||||
import AlertsScreen from "../screens/admin/AlertsScreen";
|
import AlertsScreen from "../screens/admin/AlertsScreen";
|
||||||
import AddressScreen from "../screens/admin/AddressScreen";
|
import AddressScreen from "../screens/admin/AddressScreen";
|
||||||
@@ -128,6 +129,20 @@ function AdminTabs() {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name="Categories"
|
||||||
|
component={CategoriesScreen}
|
||||||
|
options={{
|
||||||
|
title: "Catégories",
|
||||||
|
tabBarIcon: ({ color, size }) => (
|
||||||
|
<Ionicons
|
||||||
|
name="pricetag-outline"
|
||||||
|
size={size}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="Delivery"
|
name="Delivery"
|
||||||
component={DeliveryScreen}
|
component={DeliveryScreen}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type AdminTabParamList = {
|
|||||||
Orders: undefined;
|
Orders: undefined;
|
||||||
Users: undefined;
|
Users: undefined;
|
||||||
Products: undefined;
|
Products: undefined;
|
||||||
|
Categories: undefined;
|
||||||
Delivery: undefined;
|
Delivery: undefined;
|
||||||
Alerts: undefined;
|
Alerts: undefined;
|
||||||
Addresses: undefined;
|
Addresses: undefined;
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
FlatList,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
Modal,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
} from "react-native";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { spacing, fontSize, borderRadius } from "../../theme";
|
||||||
|
import { useTheme } from "../../context/ThemeContext";
|
||||||
|
import {
|
||||||
|
getCategories,
|
||||||
|
createCategoryAdmin,
|
||||||
|
updateCategoryAdmin,
|
||||||
|
deleteCategoryAdmin,
|
||||||
|
} from "../../api/api_admin";
|
||||||
|
import type { Category } from "../../api/api_admin";
|
||||||
|
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
||||||
|
import Card from "../../components/ui/Card";
|
||||||
|
import AlertModal from "../../components/ui/AlertModal";
|
||||||
|
import { useAlert } from "../../hooks/useAlert";
|
||||||
|
|
||||||
|
export default function CategoriesScreen() {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
const [editingCategory, setEditingCategory] = useState<Category | null>(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 <LoadingSpinner />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.title}>Catégories</Text>
|
||||||
|
<TouchableOpacity style={styles.addBtn} onPress={openCreate}>
|
||||||
|
<Ionicons name="add" size={18} color="#fff" />
|
||||||
|
<Text style={styles.addBtnText}>Ajouter</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<FlatList
|
||||||
|
data={categories}
|
||||||
|
keyExtractor={(item) => String(item.id)}
|
||||||
|
contentContainerStyle={styles.list}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<Text style={styles.empty}>Aucune catégorie. Créez-en une !</Text>
|
||||||
|
}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<Card>
|
||||||
|
<View style={styles.row}>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.catName}>{item.name}</Text>
|
||||||
|
<Text style={styles.catDate}>
|
||||||
|
Créée le {new Date(item.created_at).toLocaleDateString("fr-FR")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.actions}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.actionBtn}
|
||||||
|
onPress={() => openEdit(item)}
|
||||||
|
>
|
||||||
|
<Ionicons name="pencil-outline" size={20} color={colors.info} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.actionBtn}
|
||||||
|
onPress={() => handleDelete(item)}
|
||||||
|
>
|
||||||
|
<Ionicons name="trash-outline" size={20} color={colors.danger} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Modal visible={modalVisible} transparent animationType="fade">
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||||
|
style={styles.overlay}
|
||||||
|
>
|
||||||
|
<View style={styles.modal}>
|
||||||
|
<Text style={styles.modalTitle}>
|
||||||
|
{editingCategory ? "Modifier la catégorie" : "Nouvelle catégorie"}
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="Nom de la catégorie"
|
||||||
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
value={name}
|
||||||
|
onChangeText={setName}
|
||||||
|
autoCapitalize="none"
|
||||||
|
/>
|
||||||
|
<View style={styles.modalBtns}>
|
||||||
|
<TouchableOpacity style={styles.cancelBtn} onPress={closeModal}>
|
||||||
|
<Text style={styles.cancelBtnText}>Annuler</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.saveBtn}
|
||||||
|
onPress={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
<Text style={styles.saveBtnText}>
|
||||||
|
{saving ? "..." : "Enregistrer"}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<AlertModal
|
||||||
|
visible={alert.visible}
|
||||||
|
type={alert.type}
|
||||||
|
title={alert.title}
|
||||||
|
message={alert.message}
|
||||||
|
onConfirm={alert.onConfirm || hideAlert}
|
||||||
|
onCancel={alert.type === "confirm" ? hideAlert : undefined}
|
||||||
|
confirmText={alert.type === "confirm" ? "Confirmer" : "OK"}
|
||||||
|
cancelText="Annuler"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,7 +25,9 @@ import {
|
|||||||
deleteProductAdmin,
|
deleteProductAdmin,
|
||||||
uploadProductMediaAdmin,
|
uploadProductMediaAdmin,
|
||||||
deleteProductMediaAdmin,
|
deleteProductMediaAdmin,
|
||||||
|
getCategories,
|
||||||
} from "../../api/api_admin";
|
} from "../../api/api_admin";
|
||||||
|
import type { Category } from "../../api/api_admin";
|
||||||
import type { Product } from "../../api/types";
|
import type { Product } from "../../api/types";
|
||||||
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
||||||
import Card from "../../components/ui/Card";
|
import Card from "../../components/ui/Card";
|
||||||
@@ -54,11 +56,7 @@ interface PendingMedia {
|
|||||||
mediaType: "image" | "video";
|
mediaType: "image" | "video";
|
||||||
}
|
}
|
||||||
|
|
||||||
const CATEGORIES = [
|
// Les catégories sont chargées dynamiquement depuis l'API
|
||||||
{ value: "weed&hash", label: "Weed & Hash" },
|
|
||||||
{ value: "zipette&co", label: "Zipette & Co" },
|
|
||||||
{ value: "gros&semi", label: "Gros & Semi" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const UNITS = [
|
const UNITS = [
|
||||||
{ value: "u", label: "u" },
|
{ value: "u", label: "u" },
|
||||||
@@ -79,9 +77,9 @@ interface FormState {
|
|||||||
prices: PriceRow[];
|
prices: PriceRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyForm = (): FormState => ({
|
const emptyForm = (firstCategory = ""): FormState => ({
|
||||||
name: "",
|
name: "",
|
||||||
category: "weed&hash",
|
category: firstCategory,
|
||||||
description: "",
|
description: "",
|
||||||
stock: "",
|
stock: "",
|
||||||
unit: "u",
|
unit: "u",
|
||||||
@@ -94,6 +92,7 @@ const emptyForm = (): FormState => ({
|
|||||||
export default function ProductsScreen() {
|
export default function ProductsScreen() {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const [products, setProducts] = useState<Product[]>([]);
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
@@ -117,8 +116,12 @@ export default function ProductsScreen() {
|
|||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const result = await getAllProductsAdmin();
|
const [result, cats] = await Promise.all([
|
||||||
|
getAllProductsAdmin(),
|
||||||
|
getCategories(),
|
||||||
|
]);
|
||||||
setProducts(result.data);
|
setProducts(result.data);
|
||||||
|
setCategories(cats);
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
@@ -139,7 +142,7 @@ export default function ProductsScreen() {
|
|||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditingProduct(null);
|
setEditingProduct(null);
|
||||||
setForm(emptyForm());
|
setForm(emptyForm(categories[0]?.name || ""));
|
||||||
setExistingMedia([]);
|
setExistingMedia([]);
|
||||||
setMediaToDelete([]);
|
setMediaToDelete([]);
|
||||||
setPendingMedia([]);
|
setPendingMedia([]);
|
||||||
@@ -896,29 +899,29 @@ export default function ProductsScreen() {
|
|||||||
{/* Catégorie */}
|
{/* Catégorie */}
|
||||||
<Text style={styles.label}>Catégorie *</Text>
|
<Text style={styles.label}>Catégorie *</Text>
|
||||||
<View style={styles.catRow}>
|
<View style={styles.catRow}>
|
||||||
{CATEGORIES.map((c) => (
|
{categories.map((c) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={c.value}
|
key={c.id}
|
||||||
style={[
|
style={[
|
||||||
styles.catBtn,
|
styles.catBtn,
|
||||||
form.category === c.value &&
|
form.category === c.name &&
|
||||||
styles.catBtnActive,
|
styles.catBtnActive,
|
||||||
]}
|
]}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
setForm((f) => ({
|
setForm((f) => ({
|
||||||
...f,
|
...f,
|
||||||
category: c.value,
|
category: c.name,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.catBtnText,
|
styles.catBtnText,
|
||||||
form.category === c.value &&
|
form.category === c.name &&
|
||||||
styles.catBtnTextActive,
|
styles.catBtnTextActive,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{c.label}
|
{c.name}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
|
|||||||
+3
-3
@@ -15,7 +15,7 @@
|
|||||||
"simulator": true
|
"simulator": true
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"API_URL": "https://uber-stup.club"
|
"API_URL": "http://5.181.0.112"
|
||||||
},
|
},
|
||||||
"channel": "development"
|
"channel": "development"
|
||||||
},
|
},
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"buildType": "apk"
|
"buildType": "apk"
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"API_URL": "https://uber-stup.club"
|
"API_URL": "http://5.181.0.112"
|
||||||
},
|
},
|
||||||
"channel": "preview"
|
"channel": "preview"
|
||||||
},
|
},
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"buildType": "apk"
|
"buildType": "apk"
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"API_URL": "https://uber-stup.club"
|
"API_URL": "http://5.181.0.112"
|
||||||
},
|
},
|
||||||
"channel": "production"
|
"channel": "production"
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -11,7 +11,7 @@ import type {
|
|||||||
import { getToken } from "../auth/tokenStorage";
|
import { getToken } from "../auth/tokenStorage";
|
||||||
import { extractUsernameFromToken } from "../auth/jwtUtils";
|
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<string | null> => {
|
export const getJwtUsername = async (): Promise<string | null> => {
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
@@ -113,6 +113,21 @@ export const logoutUser = async (): Promise<void> => {
|
|||||||
// PRODUCTS
|
// PRODUCTS
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
|
export interface Category {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCategories = async (): Promise<Category[]> => {
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.get(`${V1}/categories`);
|
||||||
|
return data.categories || [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getAllProducts = async () => {
|
export const getAllProducts = async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await apiClient.get(`${V1}/products`);
|
const { data } = await apiClient.get(`${V1}/products`);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import axios from "axios";
|
|||||||
import { getToken, getAdminToken } from "../auth/tokenStorage";
|
import { getToken, getAdminToken } from "../auth/tokenStorage";
|
||||||
|
|
||||||
// Change this to your server IP/domain
|
// 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({
|
const apiClient = axios.create({
|
||||||
baseURL: API_BASE_URL,
|
baseURL: API_BASE_URL,
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
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 type { Product } from "../../api/api_types";
|
||||||
import ProductCard from "../../components/ProductCard";
|
import ProductCard from "../../components/ProductCard";
|
||||||
import CategoryPill from "../../components/CategoryPill";
|
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 { width: SCREEN_WIDTH } = Dimensions.get("window");
|
||||||
const CARD_WIDTH = SCREEN_WIDTH - 48;
|
const CARD_WIDTH = SCREEN_WIDTH - 48;
|
||||||
|
|
||||||
const CATEGORIES = [
|
// Les catégories sont chargées dynamiquement depuis l'API
|
||||||
{ 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<string, string> = {
|
|
||||||
tous: "Tous les produits",
|
|
||||||
"weed&hash": "Weed & Hash",
|
|
||||||
"zipette&co": "Zipette & Co",
|
|
||||||
"gros&semi": "Gros & Semi",
|
|
||||||
};
|
|
||||||
|
|
||||||
type Nav = NativeStackNavigationProp<ClientStackParamList>;
|
type Nav = NativeStackNavigationProp<ClientStackParamList>;
|
||||||
|
|
||||||
export default function ProductsScreen() {
|
export default function ProductsScreen() {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const navigation = useNavigation<Nav>();
|
const navigation = useNavigation<Nav>();
|
||||||
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [products, setProducts] = useState<Product[]>([]);
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
const [selectedCategory, setSelectedCategory] = useState("tous");
|
const [selectedCategory, setSelectedCategory] = useState("tous");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -60,6 +50,10 @@ export default function ProductsScreen() {
|
|||||||
const carouselRef = useRef<FlatList>(null);
|
const carouselRef = useRef<FlatList>(null);
|
||||||
const scaleAnim = useRef(new Animated.Value(1)).current;
|
const scaleAnim = useRef(new Animated.Value(1)).current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCategories().then(setCategories);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const pulse = Animated.loop(
|
const pulse = Animated.loop(
|
||||||
Animated.sequence([
|
Animated.sequence([
|
||||||
@@ -190,7 +184,7 @@ export default function ProductsScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageBackground
|
<ImageBackground
|
||||||
source={selectedCategory === "gros&semi" ? logoGrosSemi : undefined}
|
source={undefined}
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
imageStyle={styles.bgImage}
|
imageStyle={styles.bgImage}
|
||||||
>
|
>
|
||||||
@@ -200,19 +194,24 @@ export default function ProductsScreen() {
|
|||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.filters}
|
contentContainerStyle={styles.filters}
|
||||||
>
|
>
|
||||||
{CATEGORIES.map((cat) => (
|
<CategoryPill
|
||||||
|
label="Tous"
|
||||||
|
active={selectedCategory === "tous"}
|
||||||
|
onPress={() => setSelectedCategory("tous")}
|
||||||
|
/>
|
||||||
|
{categories.map((cat) => (
|
||||||
<CategoryPill
|
<CategoryPill
|
||||||
key={cat.value}
|
key={cat.id}
|
||||||
label={cat.label}
|
label={cat.name}
|
||||||
active={selectedCategory === cat.value}
|
active={selectedCategory === cat.name}
|
||||||
onPress={() => setSelectedCategory(cat.value)}
|
onPress={() => setSelectedCategory(cat.name)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.categoryHeader}>
|
<View style={styles.categoryHeader}>
|
||||||
<Text style={styles.categoryTitle}>
|
<Text style={styles.categoryTitle}>
|
||||||
{CATEGORY_TITLES[selectedCategory]}
|
{selectedCategory === "tous" ? "Tous les produits" : selectedCategory}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{error ? (
|
{error ? (
|
||||||
@@ -258,18 +257,6 @@ export default function ProductsScreen() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{selectedCategory === "gros&semi" && (
|
|
||||||
<View style={styles.comingSoonWrapper}>
|
|
||||||
<Animated.Text
|
|
||||||
style={[
|
|
||||||
styles.comingSoonText,
|
|
||||||
{ transform: [{ scale: scaleAnim }] },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
Prochainement
|
|
||||||
</Animated.Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</ImageBackground>
|
</ImageBackground>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user