chore: add new features for cabine

This commit is contained in:
2026-02-26 19:57:01 +01:00
parent 0fb4bd8a89
commit f91d4276cc
8 changed files with 467 additions and 323 deletions
+29 -2
View File
@@ -446,6 +446,35 @@ func (d *Database) CheckClientCanOrder(username string) (bool, float64, error) {
return true, 0, nil
}
func (d *Database) ResetClientPoint(username string, resetCancellationsPoint bool) error {
var query string
if resetCancellationsPoint {
query = `UPDATE clients SET point = 0, point_zipette = 0, updated_at = CURRENT_TIMESTAMP WHERE username = $1`
} else {
query = `UPDATE clients SET point = 0, updated_at = CURRENT_TIMESTAMP WHERE username = $1`
}
result, err := d.Exec(query, username)
if err != nil {
log.Printf("❌ [ResetClientPointAdmin] Erreur UPDATE: %v", err)
return fmt.Errorf("erreur reset points: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur vérification: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("client non trouvé")
}
// Invalider le cache Redis du client
cacheKey := fmt.Sprintf("client:%s", username)
Redis.Del(RedisCtx, cacheKey)
return nil
}
func (d *Database) ResetClientPenalties(username string, resetCancellationsCount bool) error {
log.Printf("🔄 [ResetClientPenalties] Reset pour %s (reset_count=%v)", username, resetCancellationsCount)
@@ -477,8 +506,6 @@ func (d *Database) ResetClientPenalties(username string, resetCancellationsCount
return fmt.Errorf("client non trouvé")
}
log.Printf("✅ [ResetClientPenalties] Reset effectué pour %s", username)
// Invalider le cache Redis du client
cacheKey := fmt.Sprintf("client:%s", username)
Redis.Del(RedisCtx, cacheKey)
+34 -2
View File
@@ -935,9 +935,41 @@ func GetPenaltiesStats(c *gin.Context) {
})
}
func ResetClientPointAdmin(c *gin.Context) {
userRole := c.GetString("role")
if userRole != "admin" && userRole != "cabine" {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return
}
username := c.Param("username")
if username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Username requis"})
return
}
var req struct {
ResetCancellationsPoints bool `json:"reset_cancellations_points"`
}
if err := c.ShouldBindJSON(&req); err != nil {
req.ResetCancellationsPoints = false
}
database := c.MustGet("database").(*db.Database)
err := database.ResetClientPoint(username, req.ResetCancellationsPoints)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la réinitialisation",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Points réinitialisés"})
}
// ResetClientPenaltiesAdmin réinitialise les pénalités d'un client (Admin seulement)
// POST /api/v2/admin/protected/client/:username/penalties/reset
// Body: {"reset_cancellations_count": false}
func ResetClientPenaltiesAdmin(c *gin.Context) {
userRole := c.GetString("role")
if userRole != "admin" && userRole != "cabine" {
+1 -1
View File
@@ -225,9 +225,9 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
cabineGroupV1.DELETE("/commands/:id", handlers.DeleteCommandByCabine)
// ⭐ NOUVEAU - ANNULATION PAR CABINE
cabineGroupV1.GET("/commands/cancelled", handlers.GetAllCancelledOrders)
cabineGroupV1.POST("/penalty", handlers.ApplyClientPenalty) // Appliquer pénalité
cabineGroupV1.GET("/client/:username/penalties", handlers.GetClientPenaltiesAdmin) // Voir pénalités client
cabineGroupV1.POST("/client/:username/penalties/reset", handlers.ResetClientPenaltiesAdmin) // Reset pénalités
cabineGroupV1.POST("/client/:username/point/reset", handlers.ResetClientPointAdmin)
cabineGroupV1.GET("/penalties/all", handlers.GetAllClientsWithPenalties) // Liste clients avec pénalités
cabineGroupV1.GET("/penalties/stats", handlers.GetPenaltiesStats)
+11
View File
@@ -62,6 +62,17 @@ export const resetClientPenalties = async (clientUsername: string) => {
return { success: true, message: data.message };
};
export const resetClientPoints = async (
clientUsername: string,
resetCancellationsPoints: boolean = false,
) => {
const { data } = await apiClient.post(
`${API}/client/${clientUsername}/point/reset`,
{ reset_cancellations_points: resetCancellationsPoints },
);
return { success: true, message: data.message };
};
export const getAllClientsWithPenalties = async () => {
const { data } = await apiClient.get(`${API}/penalties/all`);
return {
@@ -17,7 +17,6 @@ import {
Dimensions,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import MapView, { Marker, Polyline, PROVIDER_DEFAULT } from "react-native-maps";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import {
@@ -31,6 +30,10 @@ import { STATUS_LABELS, getStatusColors } from "../../utils/constants";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
import Card from "../../components/ui/Card";
import Badge from "../../components/ui/Badge";
import TomTomMap, {
TomTomMapRef,
TomTomMarker,
} from "../../components/TomTomMap";
const MAP_HEIGHT = 280;
@@ -48,17 +51,14 @@ export default function DeliveryScreen() {
const [refreshing, setRefreshing] = useState(false);
// Map
const mapRef = useRef<MapView | null>(null);
const fullscreenMapRef = useRef<MapView | null>(null);
const mapRef = useRef<TomTomMapRef | null>(null);
const fullscreenMapRef = useRef<TomTomMapRef | null>(null);
const [mapFullscreen, setMapFullscreen] = useState(false);
// Selected livreur route
const [selectedLivreur, setSelectedLivreur] =
useState<DeliveryPerson | null>(null);
const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
const [destinationCoords, setDestinationCoords] = useState<LatLng | null>(
null,
);
const [routeLoading, setRouteLoading] = useState(false);
const loadData = useCallback(async () => {
@@ -93,7 +93,6 @@ export default function DeliveryScreen() {
async (livreur: DeliveryPerson) => {
setSelectedLivreur(livreur);
setRouteInfo(null);
setDestinationCoords(null);
if (!livreur.stats.current_command) return;
@@ -110,44 +109,26 @@ export default function DeliveryScreen() {
return;
}
const origin: LatLng = {
latitude: livreur.location.latitude,
longitude: livreur.location.longitude,
};
const dest = await geocodeAddress(cmdAddress);
if (!dest) {
setRouteLoading(false);
return;
}
setDestinationCoords(dest);
const origin: LatLng = {
latitude: livreur.location.latitude,
longitude: livreur.location.longitude,
};
const result = await calculateRoute(origin, dest);
if (result) {
setRouteInfo(result.route);
const ref = mapFullscreen ? fullscreenMapRef : mapRef;
if (ref.current) {
ref.current.fitToCoordinates(
[
{
latitude: origin.latitude,
longitude: origin.longitude,
},
{
latitude: dest.latitude,
longitude: dest.longitude,
},
],
{
edgePadding: {
top: 80,
right: 60,
bottom: 80,
left: 60,
},
animated: true,
},
);
}
ref.current?.fitToCoordinates([origin, dest]);
ref.current?.calcRoute(origin, dest);
} else {
const ref = mapFullscreen ? fullscreenMapRef : mapRef;
ref.current?.calcRoute(origin, dest);
}
} catch {
/* silent */
@@ -160,88 +141,26 @@ export default function DeliveryScreen() {
const clearRoute = () => {
setSelectedLivreur(null);
setRouteInfo(null);
setDestinationCoords(null);
mapRef.current?.clearRoute();
fullscreenMapRef.current?.clearRoute();
};
const fitAllMarkers = (ref: React.RefObject<MapView | null>) => {
if (ref.current && livreursWithGPS.length > 0) {
ref.current.fitToCoordinates(
const livreurMarkers = useMemo<TomTomMarker[]>(
() =>
livreursWithGPS.map((l) => ({
id: l.username,
latitude: l.location.latitude,
longitude: l.location.longitude,
color:
selectedLivreur?.username === l.username
? "#2196F3"
: statusColors[l.status] || "#888",
label: l.username,
description: `${STATUS_LABELS[l.status] || l.status}${l.stats.current_command ? ` · Cmd #${l.stats.current_command}` : ""}`,
isSelected: selectedLivreur?.username === l.username,
})),
{
edgePadding: { top: 60, right: 60, bottom: 60, left: 60 },
animated: true,
},
[livreursWithGPS, selectedLivreur, statusColors],
);
}
};
// Render map markers point bleu pour chaque livreur
const renderMapMarkers = () =>
livreursWithGPS.map((l) => {
const isSelected = selectedLivreur?.username === l.username;
return (
<Marker
key={l.username}
coordinate={{
latitude: l.location.latitude,
longitude: l.location.longitude,
}}
title={l.username}
description={`${STATUS_LABELS[l.status] || l.status}${l.stats.current_command ? ` · Cmd #${l.stats.current_command}` : ""}`}
onPress={() => trackLivreur(l)}
>
<View
style={[
styles.markerOuter,
isSelected && styles.markerSelected,
]}
>
<View style={styles.markerInner}>
<Ionicons
name="bicycle"
size={14}
color={colors.white}
/>
</View>
</View>
</Marker>
);
});
const renderRouteOverlay = () => {
if (!destinationCoords) return null;
return (
<>
<Marker
coordinate={{
latitude: destinationCoords.latitude,
longitude: destinationCoords.longitude,
}}
title="Destination"
>
<View style={styles.destMarkerOuter}>
<View style={styles.destMarkerInner}>
<Ionicons
name="flag"
size={12}
color={colors.white}
/>
</View>
</View>
</Marker>
{routeInfo && routeInfo.coordinates.length > 0 && (
<Polyline
coordinates={routeInfo.coordinates}
strokeColor="#4285F4"
strokeWidth={4}
/>
)}
</>
);
};
const renderLivreur = ({ item }: { item: DeliveryPerson }) => {
const isSelected = selectedLivreur?.username === item.username;
@@ -334,22 +253,28 @@ export default function DeliveryScreen() {
<View>
{livreursWithGPS.length > 0 && (
<View style={styles.mapContainer}>
<MapView
<TomTomMap
ref={mapRef}
provider={PROVIDER_DEFAULT}
style={styles.map}
initialRegion={{
markers={livreurMarkers}
initialCenter={{
latitude: livreursWithGPS[0].location.latitude,
longitude: livreursWithGPS[0].location.longitude,
latitudeDelta: 0.05,
longitudeDelta: 0.05,
}}
onMapReady={() => fitAllMarkers(mapRef)}
showsUserLocation={false}
>
{renderMapMarkers()}
{renderRouteOverlay()}
</MapView>
initialZoom={13}
onMarkerPress={(id) => {
const livreur = livreursWithGPS.find(
(l) => l.username === id,
);
if (livreur) {
if (selectedLivreur?.username === id) {
clearRoute();
} else {
trackLivreur(livreur);
}
}
}}
/>
{routeInfo && selectedLivreur && (
<View style={styles.routeOverlay}>
@@ -392,7 +317,7 @@ export default function DeliveryScreen() {
<View style={styles.mapBtns}>
<TouchableOpacity
style={styles.mapBtn}
onPress={() => fitAllMarkers(mapRef)}
onPress={() => mapRef.current?.fitAllMarkers()}
>
<Ionicons
name="locate-outline"
@@ -469,47 +394,6 @@ export default function DeliveryScreen() {
},
map: { width: "100%", height: MAP_HEIGHT },
// Point bleu pour les livreurs
markerOuter: {
width: 34,
height: 34,
borderRadius: 17,
backgroundColor: "rgba(33, 150, 243, 0.3)",
justifyContent: "center",
alignItems: "center",
},
markerSelected: {
borderWidth: 2,
borderColor: colors.info,
width: 40,
height: 40,
borderRadius: 20,
},
markerInner: {
width: 26,
height: 26,
borderRadius: 13,
backgroundColor: "#2196F3",
justifyContent: "center",
alignItems: "center",
},
destMarkerOuter: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: colors.danger + "40",
justifyContent: "center",
alignItems: "center",
},
destMarkerInner: {
width: 22,
height: 22,
borderRadius: 11,
backgroundColor: colors.danger,
justifyContent: "center",
alignItems: "center",
},
routeOverlay: {
position: "absolute",
top: spacing.s,
@@ -746,25 +630,30 @@ export default function DeliveryScreen() {
<StatusBar hidden={mapFullscreen} />
<View style={styles.fullscreenContainer}>
{livreursWithGPS.length > 0 && (
<MapView
<TomTomMap
ref={fullscreenMapRef}
provider={PROVIDER_DEFAULT}
style={styles.fullscreenMap}
initialRegion={{
latitude: livreursWithGPS[0].location.latitude,
markers={livreurMarkers}
initialCenter={{
latitude:
livreursWithGPS[0].location.latitude,
longitude:
livreursWithGPS[0].location.longitude,
latitudeDelta: 0.05,
longitudeDelta: 0.05,
}}
onMapReady={() => fitAllMarkers(fullscreenMapRef)}
showsUserLocation={false}
showsCompass
showsScale
>
{renderMapMarkers()}
{renderRouteOverlay()}
</MapView>
initialZoom={13}
onMarkerPress={(id) => {
const livreur = livreursWithGPS.find(
(l) => l.username === id,
);
if (livreur) {
if (selectedLivreur?.username === id) {
clearRoute();
} else {
trackLivreur(livreur);
}
}
}}
/>
)}
<View style={styles.fullscreenTopBar}>
@@ -783,7 +672,9 @@ export default function DeliveryScreen() {
</Text>
<TouchableOpacity
style={styles.closeBtn}
onPress={() => fitAllMarkers(fullscreenMapRef)}
onPress={() =>
fullscreenMapRef.current?.fitAllMarkers()
}
>
<Ionicons
name="locate-outline"
@@ -4,15 +4,15 @@ import {
Text,
StyleSheet,
FlatList,
TouchableOpacity,
RefreshControl,
ScrollView,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import { getAllCommands } from "../../api/api_admin";
import {
getCommandItems,
updateItemStatus,
deleteCommand,
} from "../../api/api_cabine";
import type { CommandResponse } from "../../api/types";
@@ -24,7 +24,23 @@ import Card from "../../components/ui/Card";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
const ITEM_STATUSES = ["pending", "preparing", "ready"];
const STATUS_COLORS: Record<string, string> = {
pending: "#F59E0B",
preparing: "#3B82F6",
ready: "#10B981",
};
const STATUS_ICONS: Record<string, keyof typeof Ionicons.glyphMap> = {
pending: "time-outline",
preparing: "construct-outline",
ready: "checkmark-circle-outline",
};
const STATUS_LABELS: Record<string, string> = {
pending: "En attente",
preparing: "En préparation",
ready: "Prêt",
};
export default function OrdersScreen() {
const { colors } = useTheme();
@@ -36,7 +52,15 @@ export default function OrdersScreen() {
visible: boolean;
commandId: number | null;
items: any[];
}>({ visible: false, commandId: null, items: [] });
commandInfo: any;
clientInfo: any;
}>({
visible: false,
commandId: null,
items: [],
commandInfo: null,
clientInfo: null,
});
const loadData = useCallback(async () => {
try {
@@ -56,6 +80,7 @@ export default function OrdersScreen() {
useEffect(() => {
loadData();
}, [loadData]);
const onRefresh = async () => {
setRefreshing(true);
await loadData();
@@ -65,20 +90,18 @@ export default function OrdersScreen() {
const openItems = async (commandId: number) => {
try {
const result = await getCommandItems(commandId);
setItemsModal({ visible: true, commandId, items: result.items });
setItemsModal({
visible: true,
commandId,
items: result.items,
commandInfo: result.command_info,
clientInfo: result.client_info,
});
} catch (e: any) {
showError("Erreur", e.message);
}
};
const handleItemStatus = async (itemId: number, status: string) => {
try {
await updateItemStatus(itemId, status);
if (itemsModal.commandId) await openItems(itemsModal.commandId);
} catch (e: any) {
showError("Erreur", e.message);
}
};
const handleDelete = (commandId: number) => {
showConfirm(
@@ -96,6 +119,15 @@ export default function OrdersScreen() {
);
};
const closeModal = () =>
setItemsModal({
visible: false,
commandId: null,
items: [],
commandInfo: null,
clientInfo: null,
});
const styles = useMemo(
() =>
StyleSheet.create({
@@ -121,24 +153,124 @@ export default function OrdersScreen() {
gap: spacing.s,
marginTop: spacing.m,
},
itemRow: {
flexDirection: "row",
alignItems: "center",
padding: spacing.m,
backgroundColor: colors.bgCard,
borderRadius: borderRadius.sm,
marginBottom: spacing.s,
},
itemName: {
color: colors.textWhite,
fontSize: fontSize.md,
fontWeight: "600",
},
empty: {
color: colors.textMuted,
textAlign: "center",
marginTop: spacing.xl,
},
// Modal summary
modalSummary: {
backgroundColor: colors.bgCard,
borderRadius: borderRadius.sm,
padding: spacing.m,
marginBottom: spacing.m,
gap: spacing.xs,
},
modalSummaryRow: {
flexDirection: "row",
alignItems: "center",
gap: spacing.s,
},
modalSummaryText: {
color: colors.textSecondary,
fontSize: fontSize.sm,
flex: 1,
},
modalTotal: {
color: colors.accent,
fontSize: fontSize.md,
fontWeight: "700",
marginTop: spacing.xs,
},
// Modal progress bar
progressContainer: {
marginBottom: spacing.m,
},
progressLabel: {
color: colors.textMuted,
fontSize: fontSize.xs,
marginBottom: spacing.xs,
},
progressBar: {
height: 4,
backgroundColor: colors.bgCard,
borderRadius: 2,
overflow: "hidden",
},
progressFill: {
height: "100%",
borderRadius: 2,
},
// Item card
itemCard: {
backgroundColor: colors.bgCard,
borderRadius: borderRadius.sm,
marginBottom: spacing.s,
overflow: "hidden",
},
itemCardAccent: {
height: 3,
},
itemCardBody: {
padding: spacing.m,
flexDirection: "row",
alignItems: "flex-start",
gap: spacing.m,
},
itemIndex: {
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: colors.bgPrimary,
justifyContent: "center",
alignItems: "center",
marginTop: 2,
},
itemIndexText: {
color: colors.textMuted,
fontSize: fontSize.xs,
fontWeight: "700",
},
itemInfo: { flex: 1 },
itemName: {
color: colors.textWhite,
fontSize: fontSize.md,
fontWeight: "600",
marginBottom: 2,
},
itemMeta: {
color: colors.textMuted,
fontSize: fontSize.xs,
marginBottom: spacing.s,
},
itemStatusRow: {
flexDirection: "row",
alignItems: "center",
gap: 5,
},
itemStatusText: {
fontSize: fontSize.xs,
fontWeight: "600",
},
itemActions: {
gap: spacing.xs,
},
statusBtn: {
flexDirection: "row",
alignItems: "center",
gap: 4,
paddingHorizontal: spacing.s,
paddingVertical: 5,
borderRadius: borderRadius.sm,
borderWidth: 1,
},
statusBtnText: {
fontSize: fontSize.xs,
fontWeight: "600",
},
}),
[colors],
);
@@ -171,6 +303,12 @@ export default function OrdersScreen() {
</Card>
);
const readyCount = itemsModal.items.filter(
(i) => i.status === "ready",
).length;
const totalCount = itemsModal.items.length;
const progress = totalCount > 0 ? readyCount / totalCount : 0;
if (loading) return <LoadingSpinner message="Chargement..." />;
return (
@@ -191,51 +329,138 @@ export default function OrdersScreen() {
<Text style={styles.empty}>Aucune commande active</Text>
}
/>
<Modal
visible={itemsModal.visible}
onClose={() =>
setItemsModal({
visible: false,
commandId: null,
items: [],
})
}
title={`Items #${itemsModal.commandId}`}
icon="list-outline"
onClose={closeModal}
title={`Commande #${itemsModal.commandId}`}
icon="receipt-outline"
>
{itemsModal.items.map((item: any) => (
<View key={item.id} style={styles.itemRow}>
<View style={{ flex: 1 }}>
<Text style={styles.itemName}>
{item.product_name}
<ScrollView
showsVerticalScrollIndicator={false}
bounces={false}
>
{/* Résumé commande / client */}
{(itemsModal.commandInfo || itemsModal.clientInfo) && (
<View style={styles.modalSummary}>
{itemsModal.clientInfo?.username && (
<View style={styles.modalSummaryRow}>
<Ionicons
name="person-outline"
size={14}
color={colors.textMuted}
/>
<Text style={styles.modalSummaryText}>
{itemsModal.clientInfo.prenom}{" "}
{itemsModal.clientInfo.nom} ·{" "}
{itemsModal.clientInfo.username}
</Text>
<Text style={styles.info}>
Qté: {item.quantity} | {item.price?.toFixed(2)}{" "}
</View>
)}
{(itemsModal.commandInfo?.address || itemsModal.commandInfo?.adresse) && (
<View style={styles.modalSummaryRow}>
<Ionicons
name="location-outline"
size={14}
color={colors.textMuted}
/>
<Text style={styles.modalSummaryText}>
{itemsModal.commandInfo.address || itemsModal.commandInfo.adresse}
</Text>
</View>
)}
{itemsModal.commandInfo?.total_prix != null && (
<Text style={styles.modalTotal}>
{Number(
itemsModal.commandInfo.total_prix,
).toFixed(2)}{" "}
</Text>
<StatusBadge status={item.status} />
</View>
<View style={{ gap: spacing.xs }}>
{ITEM_STATUSES.filter((s) => s !== item.status).map(
(s) => (
<Button
key={s}
title={s}
onPress={() =>
handleItemStatus(item.id, s)
}
size="sm"
variant="outline"
/>
),
)}
</View>
)}
{/* Barre de progression */}
{totalCount > 0 && (
<View style={styles.progressContainer}>
<Text style={styles.progressLabel}>
{readyCount}/{totalCount} prêts
</Text>
<View style={styles.progressBar}>
<View
style={[
styles.progressFill,
{
width: `${progress * 100}%`,
backgroundColor:
progress === 1
? STATUS_COLORS.ready
: colors.info,
},
]}
/>
</View>
))}
</View>
)}
{/* Liste des items */}
{itemsModal.items.map((item: any, index: number) => {
const statusColor =
STATUS_COLORS[item.status] || colors.textMuted;
return (
<View key={item.id} style={styles.itemCard}>
<View
style={[
styles.itemCardAccent,
{ backgroundColor: statusColor },
]}
/>
<View style={styles.itemCardBody}>
<View style={styles.itemIndex}>
<Text style={styles.itemIndexText}>
{index + 1}
</Text>
</View>
<View style={styles.itemInfo}>
<Text style={styles.itemName}>
{item.produit ?? item.product_name}
</Text>
<Text style={styles.itemMeta}>
Qté: {item.quantite ?? item.quantity} ·{" "}
{(item.prix ?? item.price)?.toFixed(2)}
</Text>
<View style={styles.itemStatusRow}>
<Ionicons
name={
STATUS_ICONS[
item.status
] || "ellipse-outline"
}
size={13}
color={statusColor}
/>
<Text
style={[
styles.itemStatusText,
{ color: statusColor },
]}
>
{STATUS_LABELS[item.status] ||
item.status}
</Text>
</View>
</View>
</View>
</View>
);
})}
{itemsModal.items.length === 0 && (
<Text style={styles.empty}>Aucun item</Text>
)}
</ScrollView>
</Modal>
<AlertModal
visible={alert.visible}
type={alert.type}
@@ -3,13 +3,11 @@ import { View, Text, StyleSheet, FlatList, RefreshControl } from "react-native";
import { spacing, fontSize } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import { getAllClients } from "../../api/api_admin";
import { applyClientPenalty, resetClientPenalties } from "../../api/api_cabine";
import { applyClientPenalty, resetClientPenalties, resetClientPoints } from "../../api/api_cabine";
import type { ClientResponse } from "../../api/types";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
import Card from "../../components/ui/Card";
import Button from "../../components/ui/Button";
import Modal from "../../components/ui/Modal";
import TextInput from "../../components/ui/TextInput";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
@@ -22,8 +20,6 @@ export default function UsersScreen() {
visible: boolean;
username: string;
}>({ visible: false, username: "" });
const [reason, setReason] = useState("");
const [penaltyAmount, setPenaltyAmount] = useState("");
const { alert, showError, showSuccess, showConfirm, hideAlert } =
useAlert();
@@ -45,31 +41,9 @@ export default function UsersScreen() {
setRefreshing(false);
};
const handleApplyPenalty = async () => {
if (!reason.trim()) {
showError("Erreur", "Raison requise");
return;
}
const amount = parseInt(penaltyAmount, 10);
if (!penaltyAmount.trim() || isNaN(amount) || amount <= 0) {
showError("Erreur", "Nombre de points invalide");
return;
}
try {
await applyClientPenalty(penaltyModal.username, reason, amount);
setPenaltyModal({ visible: false, username: "" });
setReason("");
setPenaltyAmount("");
await loadData();
showSuccess("Succès", "Pénalité appliquée");
} catch (e: any) {
showError("Erreur", e.message);
}
};
const handleReset = (username: string) => {
showConfirm(
"Reset",
"Reset pénalités",
`Réinitialiser les pénalités de ${username} ?`,
async () => {
try {
@@ -83,6 +57,22 @@ export default function UsersScreen() {
);
};
const handleResetPoints = (username: string) => {
showConfirm(
"Reset points",
`Réinitialiser les points de ${username} ?`,
async () => {
try {
await resetClientPoints(username);
await loadData();
} catch (e: any) {
showError("Erreur", e.message);
}
},
"Reset",
);
};
const styles = useMemo(
() =>
StyleSheet.create({
@@ -153,21 +143,14 @@ export default function UsersScreen() {
</View>
<View style={styles.actions}>
<Button
title="Pénalité"
onPress={() => {
setPenaltyModal({
visible: true,
username: item.username,
});
setReason("");
setPenaltyAmount("");
}}
title="Reset pénalités"
onPress={() => handleReset(item.username)}
size="sm"
variant="danger"
variant="outline"
/>
<Button
title="Reset"
onPress={() => handleReset(item.username)}
title="Reset points"
onPress={() => handleResetPoints(item.username)}
size="sm"
variant="outline"
/>
@@ -195,35 +178,7 @@ export default function UsersScreen() {
<Text style={styles.empty}>Aucun client</Text>
}
/>
<Modal
visible={penaltyModal.visible}
onClose={() =>
setPenaltyModal({ visible: false, username: "" })
}
title={`Pénalité - ${penaltyModal.username}`}
icon="warning-outline"
iconColor={colors.danger}
>
<TextInput
label="Nombre de points"
value={penaltyAmount}
onChangeText={setPenaltyAmount}
placeholder="Ex: 5"
keyboardType="numeric"
/>
<TextInput
label="Raison"
value={reason}
onChangeText={setReason}
placeholder="Raison de la pénalité"
/>
<Button
title="Appliquer"
onPress={handleApplyPenalty}
variant="danger"
fullWidth
/>
</Modal>
<AlertModal
visible={alert.visible}
type={alert.type}
@@ -434,7 +434,10 @@ export default function DashboardScreen() {
const openNavigation = (address: string) => {
const encoded = encodeURIComponent(address);
const url = `https://www.google.com/maps/dir/?api=1&destination=${encoded}&travelmode=driving`;
let url = `https://waze.com/ul?q=${encoded}&navigate=yes`;
if (lastCoords) {
url += `&ll=${lastCoords.lat},${lastCoords.lng}`;
}
Linking.openURL(url);
};