chore: add new features for cabine
This commit is contained in:
@@ -446,6 +446,35 @@ func (d *Database) CheckClientCanOrder(username string) (bool, float64, error) {
|
|||||||
return true, 0, nil
|
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 {
|
func (d *Database) ResetClientPenalties(username string, resetCancellationsCount bool) error {
|
||||||
log.Printf("🔄 [ResetClientPenalties] Reset pour %s (reset_count=%v)", username, resetCancellationsCount)
|
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é")
|
return fmt.Errorf("client non trouvé")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("✅ [ResetClientPenalties] Reset effectué pour %s", username)
|
|
||||||
|
|
||||||
// Invalider le cache Redis du client
|
// Invalider le cache Redis du client
|
||||||
cacheKey := fmt.Sprintf("client:%s", username)
|
cacheKey := fmt.Sprintf("client:%s", username)
|
||||||
Redis.Del(RedisCtx, cacheKey)
|
Redis.Del(RedisCtx, cacheKey)
|
||||||
|
|||||||
@@ -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)
|
// 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) {
|
func ResetClientPenaltiesAdmin(c *gin.Context) {
|
||||||
userRole := c.GetString("role")
|
userRole := c.GetString("role")
|
||||||
if userRole != "admin" && userRole != "cabine" {
|
if userRole != "admin" && userRole != "cabine" {
|
||||||
|
|||||||
@@ -225,9 +225,9 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
|
|||||||
cabineGroupV1.DELETE("/commands/:id", handlers.DeleteCommandByCabine)
|
cabineGroupV1.DELETE("/commands/:id", handlers.DeleteCommandByCabine)
|
||||||
// ⭐ NOUVEAU - ANNULATION PAR CABINE
|
// ⭐ NOUVEAU - ANNULATION PAR CABINE
|
||||||
cabineGroupV1.GET("/commands/cancelled", handlers.GetAllCancelledOrders)
|
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.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/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/all", handlers.GetAllClientsWithPenalties) // Liste clients avec pénalités
|
||||||
cabineGroupV1.GET("/penalties/stats", handlers.GetPenaltiesStats)
|
cabineGroupV1.GET("/penalties/stats", handlers.GetPenaltiesStats)
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,17 @@ export const resetClientPenalties = async (clientUsername: string) => {
|
|||||||
return { success: true, message: data.message };
|
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 () => {
|
export const getAllClientsWithPenalties = async () => {
|
||||||
const { data } = await apiClient.get(`${API}/penalties/all`);
|
const { data } = await apiClient.get(`${API}/penalties/all`);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
Dimensions,
|
Dimensions,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import MapView, { Marker, Polyline, PROVIDER_DEFAULT } from "react-native-maps";
|
|
||||||
import { spacing, fontSize, borderRadius } from "../../theme";
|
import { spacing, fontSize, borderRadius } from "../../theme";
|
||||||
import { useTheme } from "../../context/ThemeContext";
|
import { useTheme } from "../../context/ThemeContext";
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +30,10 @@ import { STATUS_LABELS, getStatusColors } from "../../utils/constants";
|
|||||||
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
||||||
import Card from "../../components/ui/Card";
|
import Card from "../../components/ui/Card";
|
||||||
import Badge from "../../components/ui/Badge";
|
import Badge from "../../components/ui/Badge";
|
||||||
|
import TomTomMap, {
|
||||||
|
TomTomMapRef,
|
||||||
|
TomTomMarker,
|
||||||
|
} from "../../components/TomTomMap";
|
||||||
|
|
||||||
const MAP_HEIGHT = 280;
|
const MAP_HEIGHT = 280;
|
||||||
|
|
||||||
@@ -48,17 +51,14 @@ export default function DeliveryScreen() {
|
|||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
// Map
|
// Map
|
||||||
const mapRef = useRef<MapView | null>(null);
|
const mapRef = useRef<TomTomMapRef | null>(null);
|
||||||
const fullscreenMapRef = useRef<MapView | null>(null);
|
const fullscreenMapRef = useRef<TomTomMapRef | null>(null);
|
||||||
const [mapFullscreen, setMapFullscreen] = useState(false);
|
const [mapFullscreen, setMapFullscreen] = useState(false);
|
||||||
|
|
||||||
// Selected livreur route
|
// Selected livreur route
|
||||||
const [selectedLivreur, setSelectedLivreur] =
|
const [selectedLivreur, setSelectedLivreur] =
|
||||||
useState<DeliveryPerson | null>(null);
|
useState<DeliveryPerson | null>(null);
|
||||||
const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
|
const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
|
||||||
const [destinationCoords, setDestinationCoords] = useState<LatLng | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
const [routeLoading, setRouteLoading] = useState(false);
|
const [routeLoading, setRouteLoading] = useState(false);
|
||||||
|
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
@@ -93,7 +93,6 @@ export default function DeliveryScreen() {
|
|||||||
async (livreur: DeliveryPerson) => {
|
async (livreur: DeliveryPerson) => {
|
||||||
setSelectedLivreur(livreur);
|
setSelectedLivreur(livreur);
|
||||||
setRouteInfo(null);
|
setRouteInfo(null);
|
||||||
setDestinationCoords(null);
|
|
||||||
|
|
||||||
if (!livreur.stats.current_command) return;
|
if (!livreur.stats.current_command) return;
|
||||||
|
|
||||||
@@ -110,44 +109,26 @@ export default function DeliveryScreen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const origin: LatLng = {
|
||||||
|
latitude: livreur.location.latitude,
|
||||||
|
longitude: livreur.location.longitude,
|
||||||
|
};
|
||||||
|
|
||||||
const dest = await geocodeAddress(cmdAddress);
|
const dest = await geocodeAddress(cmdAddress);
|
||||||
if (!dest) {
|
if (!dest) {
|
||||||
setRouteLoading(false);
|
setRouteLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setDestinationCoords(dest);
|
|
||||||
|
|
||||||
const origin: LatLng = {
|
|
||||||
latitude: livreur.location.latitude,
|
|
||||||
longitude: livreur.location.longitude,
|
|
||||||
};
|
|
||||||
const result = await calculateRoute(origin, dest);
|
const result = await calculateRoute(origin, dest);
|
||||||
if (result) {
|
if (result) {
|
||||||
setRouteInfo(result.route);
|
setRouteInfo(result.route);
|
||||||
const ref = mapFullscreen ? fullscreenMapRef : mapRef;
|
const ref = mapFullscreen ? fullscreenMapRef : mapRef;
|
||||||
if (ref.current) {
|
ref.current?.fitToCoordinates([origin, dest]);
|
||||||
ref.current.fitToCoordinates(
|
ref.current?.calcRoute(origin, dest);
|
||||||
[
|
} else {
|
||||||
{
|
const ref = mapFullscreen ? fullscreenMapRef : mapRef;
|
||||||
latitude: origin.latitude,
|
ref.current?.calcRoute(origin, dest);
|
||||||
longitude: origin.longitude,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
latitude: dest.latitude,
|
|
||||||
longitude: dest.longitude,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
edgePadding: {
|
|
||||||
top: 80,
|
|
||||||
right: 60,
|
|
||||||
bottom: 80,
|
|
||||||
left: 60,
|
|
||||||
},
|
|
||||||
animated: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* silent */
|
/* silent */
|
||||||
@@ -160,88 +141,26 @@ export default function DeliveryScreen() {
|
|||||||
const clearRoute = () => {
|
const clearRoute = () => {
|
||||||
setSelectedLivreur(null);
|
setSelectedLivreur(null);
|
||||||
setRouteInfo(null);
|
setRouteInfo(null);
|
||||||
setDestinationCoords(null);
|
mapRef.current?.clearRoute();
|
||||||
|
fullscreenMapRef.current?.clearRoute();
|
||||||
};
|
};
|
||||||
|
|
||||||
const fitAllMarkers = (ref: React.RefObject<MapView | null>) => {
|
const livreurMarkers = useMemo<TomTomMarker[]>(
|
||||||
if (ref.current && livreursWithGPS.length > 0) {
|
() =>
|
||||||
ref.current.fitToCoordinates(
|
|
||||||
livreursWithGPS.map((l) => ({
|
livreursWithGPS.map((l) => ({
|
||||||
|
id: l.username,
|
||||||
latitude: l.location.latitude,
|
latitude: l.location.latitude,
|
||||||
longitude: l.location.longitude,
|
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,
|
||||||
})),
|
})),
|
||||||
{
|
[livreursWithGPS, selectedLivreur, statusColors],
|
||||||
edgePadding: { top: 60, right: 60, bottom: 60, left: 60 },
|
|
||||||
animated: true,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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 renderLivreur = ({ item }: { item: DeliveryPerson }) => {
|
||||||
const isSelected = selectedLivreur?.username === item.username;
|
const isSelected = selectedLivreur?.username === item.username;
|
||||||
@@ -334,22 +253,28 @@ export default function DeliveryScreen() {
|
|||||||
<View>
|
<View>
|
||||||
{livreursWithGPS.length > 0 && (
|
{livreursWithGPS.length > 0 && (
|
||||||
<View style={styles.mapContainer}>
|
<View style={styles.mapContainer}>
|
||||||
<MapView
|
<TomTomMap
|
||||||
ref={mapRef}
|
ref={mapRef}
|
||||||
provider={PROVIDER_DEFAULT}
|
|
||||||
style={styles.map}
|
style={styles.map}
|
||||||
initialRegion={{
|
markers={livreurMarkers}
|
||||||
|
initialCenter={{
|
||||||
latitude: livreursWithGPS[0].location.latitude,
|
latitude: livreursWithGPS[0].location.latitude,
|
||||||
longitude: livreursWithGPS[0].location.longitude,
|
longitude: livreursWithGPS[0].location.longitude,
|
||||||
latitudeDelta: 0.05,
|
|
||||||
longitudeDelta: 0.05,
|
|
||||||
}}
|
}}
|
||||||
onMapReady={() => fitAllMarkers(mapRef)}
|
initialZoom={13}
|
||||||
showsUserLocation={false}
|
onMarkerPress={(id) => {
|
||||||
>
|
const livreur = livreursWithGPS.find(
|
||||||
{renderMapMarkers()}
|
(l) => l.username === id,
|
||||||
{renderRouteOverlay()}
|
);
|
||||||
</MapView>
|
if (livreur) {
|
||||||
|
if (selectedLivreur?.username === id) {
|
||||||
|
clearRoute();
|
||||||
|
} else {
|
||||||
|
trackLivreur(livreur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{routeInfo && selectedLivreur && (
|
{routeInfo && selectedLivreur && (
|
||||||
<View style={styles.routeOverlay}>
|
<View style={styles.routeOverlay}>
|
||||||
@@ -392,7 +317,7 @@ export default function DeliveryScreen() {
|
|||||||
<View style={styles.mapBtns}>
|
<View style={styles.mapBtns}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.mapBtn}
|
style={styles.mapBtn}
|
||||||
onPress={() => fitAllMarkers(mapRef)}
|
onPress={() => mapRef.current?.fitAllMarkers()}
|
||||||
>
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="locate-outline"
|
name="locate-outline"
|
||||||
@@ -469,47 +394,6 @@ export default function DeliveryScreen() {
|
|||||||
},
|
},
|
||||||
map: { width: "100%", height: MAP_HEIGHT },
|
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: {
|
routeOverlay: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: spacing.s,
|
top: spacing.s,
|
||||||
@@ -746,25 +630,30 @@ export default function DeliveryScreen() {
|
|||||||
<StatusBar hidden={mapFullscreen} />
|
<StatusBar hidden={mapFullscreen} />
|
||||||
<View style={styles.fullscreenContainer}>
|
<View style={styles.fullscreenContainer}>
|
||||||
{livreursWithGPS.length > 0 && (
|
{livreursWithGPS.length > 0 && (
|
||||||
<MapView
|
<TomTomMap
|
||||||
ref={fullscreenMapRef}
|
ref={fullscreenMapRef}
|
||||||
provider={PROVIDER_DEFAULT}
|
|
||||||
style={styles.fullscreenMap}
|
style={styles.fullscreenMap}
|
||||||
initialRegion={{
|
markers={livreurMarkers}
|
||||||
latitude: livreursWithGPS[0].location.latitude,
|
initialCenter={{
|
||||||
|
latitude:
|
||||||
|
livreursWithGPS[0].location.latitude,
|
||||||
longitude:
|
longitude:
|
||||||
livreursWithGPS[0].location.longitude,
|
livreursWithGPS[0].location.longitude,
|
||||||
latitudeDelta: 0.05,
|
|
||||||
longitudeDelta: 0.05,
|
|
||||||
}}
|
}}
|
||||||
onMapReady={() => fitAllMarkers(fullscreenMapRef)}
|
initialZoom={13}
|
||||||
showsUserLocation={false}
|
onMarkerPress={(id) => {
|
||||||
showsCompass
|
const livreur = livreursWithGPS.find(
|
||||||
showsScale
|
(l) => l.username === id,
|
||||||
>
|
);
|
||||||
{renderMapMarkers()}
|
if (livreur) {
|
||||||
{renderRouteOverlay()}
|
if (selectedLivreur?.username === id) {
|
||||||
</MapView>
|
clearRoute();
|
||||||
|
} else {
|
||||||
|
trackLivreur(livreur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={styles.fullscreenTopBar}>
|
<View style={styles.fullscreenTopBar}>
|
||||||
@@ -783,7 +672,9 @@ export default function DeliveryScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.closeBtn}
|
style={styles.closeBtn}
|
||||||
onPress={() => fitAllMarkers(fullscreenMapRef)}
|
onPress={() =>
|
||||||
|
fullscreenMapRef.current?.fitAllMarkers()
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="locate-outline"
|
name="locate-outline"
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
FlatList,
|
FlatList,
|
||||||
TouchableOpacity,
|
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
|
ScrollView,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { spacing, fontSize, borderRadius } from "../../theme";
|
import { spacing, fontSize, borderRadius } from "../../theme";
|
||||||
import { useTheme } from "../../context/ThemeContext";
|
import { useTheme } from "../../context/ThemeContext";
|
||||||
import { getAllCommands } from "../../api/api_admin";
|
import { getAllCommands } from "../../api/api_admin";
|
||||||
import {
|
import {
|
||||||
getCommandItems,
|
getCommandItems,
|
||||||
updateItemStatus,
|
|
||||||
deleteCommand,
|
deleteCommand,
|
||||||
} from "../../api/api_cabine";
|
} from "../../api/api_cabine";
|
||||||
import type { CommandResponse } from "../../api/types";
|
import type { CommandResponse } from "../../api/types";
|
||||||
@@ -24,7 +24,23 @@ import Card from "../../components/ui/Card";
|
|||||||
import AlertModal from "../../components/ui/AlertModal";
|
import AlertModal from "../../components/ui/AlertModal";
|
||||||
import { useAlert } from "../../hooks/useAlert";
|
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() {
|
export default function OrdersScreen() {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
@@ -36,7 +52,15 @@ export default function OrdersScreen() {
|
|||||||
visible: boolean;
|
visible: boolean;
|
||||||
commandId: number | null;
|
commandId: number | null;
|
||||||
items: any[];
|
items: any[];
|
||||||
}>({ visible: false, commandId: null, items: [] });
|
commandInfo: any;
|
||||||
|
clientInfo: any;
|
||||||
|
}>({
|
||||||
|
visible: false,
|
||||||
|
commandId: null,
|
||||||
|
items: [],
|
||||||
|
commandInfo: null,
|
||||||
|
clientInfo: null,
|
||||||
|
});
|
||||||
|
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -56,6 +80,7 @@ export default function OrdersScreen() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
}, [loadData]);
|
}, [loadData]);
|
||||||
|
|
||||||
const onRefresh = async () => {
|
const onRefresh = async () => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
await loadData();
|
await loadData();
|
||||||
@@ -65,20 +90,18 @@ export default function OrdersScreen() {
|
|||||||
const openItems = async (commandId: number) => {
|
const openItems = async (commandId: number) => {
|
||||||
try {
|
try {
|
||||||
const result = await getCommandItems(commandId);
|
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) {
|
} catch (e: any) {
|
||||||
showError("Erreur", e.message);
|
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) => {
|
const handleDelete = (commandId: number) => {
|
||||||
showConfirm(
|
showConfirm(
|
||||||
@@ -96,6 +119,15 @@ export default function OrdersScreen() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeModal = () =>
|
||||||
|
setItemsModal({
|
||||||
|
visible: false,
|
||||||
|
commandId: null,
|
||||||
|
items: [],
|
||||||
|
commandInfo: null,
|
||||||
|
clientInfo: null,
|
||||||
|
});
|
||||||
|
|
||||||
const styles = useMemo(
|
const styles = useMemo(
|
||||||
() =>
|
() =>
|
||||||
StyleSheet.create({
|
StyleSheet.create({
|
||||||
@@ -121,24 +153,124 @@ export default function OrdersScreen() {
|
|||||||
gap: spacing.s,
|
gap: spacing.s,
|
||||||
marginTop: spacing.m,
|
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: {
|
empty: {
|
||||||
color: colors.textMuted,
|
color: colors.textMuted,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
marginTop: spacing.xl,
|
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],
|
[colors],
|
||||||
);
|
);
|
||||||
@@ -171,6 +303,12 @@ export default function OrdersScreen() {
|
|||||||
</Card>
|
</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..." />;
|
if (loading) return <LoadingSpinner message="Chargement..." />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -191,51 +329,138 @@ export default function OrdersScreen() {
|
|||||||
<Text style={styles.empty}>Aucune commande active</Text>
|
<Text style={styles.empty}>Aucune commande active</Text>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
visible={itemsModal.visible}
|
visible={itemsModal.visible}
|
||||||
onClose={() =>
|
onClose={closeModal}
|
||||||
setItemsModal({
|
title={`Commande #${itemsModal.commandId}`}
|
||||||
visible: false,
|
icon="receipt-outline"
|
||||||
commandId: null,
|
|
||||||
items: [],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
title={`Items #${itemsModal.commandId}`}
|
|
||||||
icon="list-outline"
|
|
||||||
>
|
>
|
||||||
{itemsModal.items.map((item: any) => (
|
<ScrollView
|
||||||
<View key={item.id} style={styles.itemRow}>
|
showsVerticalScrollIndicator={false}
|
||||||
<View style={{ flex: 1 }}>
|
bounces={false}
|
||||||
<Text style={styles.itemName}>
|
>
|
||||||
{item.product_name}
|
{/* 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>
|
||||||
<Text style={styles.info}>
|
</View>
|
||||||
Qté: {item.quantity} | {item.price?.toFixed(2)}{" "}
|
)}
|
||||||
|
{(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>
|
</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>
|
</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>
|
||||||
))}
|
</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 && (
|
{itemsModal.items.length === 0 && (
|
||||||
<Text style={styles.empty}>Aucun item</Text>
|
<Text style={styles.empty}>Aucun item</Text>
|
||||||
)}
|
)}
|
||||||
|
</ScrollView>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<AlertModal
|
<AlertModal
|
||||||
visible={alert.visible}
|
visible={alert.visible}
|
||||||
type={alert.type}
|
type={alert.type}
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ import { View, Text, StyleSheet, FlatList, RefreshControl } from "react-native";
|
|||||||
import { spacing, fontSize } from "../../theme";
|
import { spacing, fontSize } from "../../theme";
|
||||||
import { useTheme } from "../../context/ThemeContext";
|
import { useTheme } from "../../context/ThemeContext";
|
||||||
import { getAllClients } from "../../api/api_admin";
|
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 type { ClientResponse } 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";
|
||||||
import Button from "../../components/ui/Button";
|
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 AlertModal from "../../components/ui/AlertModal";
|
||||||
import { useAlert } from "../../hooks/useAlert";
|
import { useAlert } from "../../hooks/useAlert";
|
||||||
|
|
||||||
@@ -22,8 +20,6 @@ export default function UsersScreen() {
|
|||||||
visible: boolean;
|
visible: boolean;
|
||||||
username: string;
|
username: string;
|
||||||
}>({ visible: false, username: "" });
|
}>({ visible: false, username: "" });
|
||||||
const [reason, setReason] = useState("");
|
|
||||||
const [penaltyAmount, setPenaltyAmount] = useState("");
|
|
||||||
const { alert, showError, showSuccess, showConfirm, hideAlert } =
|
const { alert, showError, showSuccess, showConfirm, hideAlert } =
|
||||||
useAlert();
|
useAlert();
|
||||||
|
|
||||||
@@ -45,31 +41,9 @@ export default function UsersScreen() {
|
|||||||
setRefreshing(false);
|
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) => {
|
const handleReset = (username: string) => {
|
||||||
showConfirm(
|
showConfirm(
|
||||||
"Reset",
|
"Reset pénalités",
|
||||||
`Réinitialiser les pénalités de ${username} ?`,
|
`Réinitialiser les pénalités de ${username} ?`,
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
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(
|
const styles = useMemo(
|
||||||
() =>
|
() =>
|
||||||
StyleSheet.create({
|
StyleSheet.create({
|
||||||
@@ -153,21 +143,14 @@ export default function UsersScreen() {
|
|||||||
</View>
|
</View>
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
<Button
|
<Button
|
||||||
title="Pénalité"
|
title="Reset pénalités"
|
||||||
onPress={() => {
|
onPress={() => handleReset(item.username)}
|
||||||
setPenaltyModal({
|
|
||||||
visible: true,
|
|
||||||
username: item.username,
|
|
||||||
});
|
|
||||||
setReason("");
|
|
||||||
setPenaltyAmount("");
|
|
||||||
}}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="danger"
|
variant="outline"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
title="Reset"
|
title="Reset points"
|
||||||
onPress={() => handleReset(item.username)}
|
onPress={() => handleResetPoints(item.username)}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
/>
|
/>
|
||||||
@@ -195,35 +178,7 @@ export default function UsersScreen() {
|
|||||||
<Text style={styles.empty}>Aucun client</Text>
|
<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
|
<AlertModal
|
||||||
visible={alert.visible}
|
visible={alert.visible}
|
||||||
type={alert.type}
|
type={alert.type}
|
||||||
|
|||||||
@@ -434,7 +434,10 @@ export default function DashboardScreen() {
|
|||||||
|
|
||||||
const openNavigation = (address: string) => {
|
const openNavigation = (address: string) => {
|
||||||
const encoded = encodeURIComponent(address);
|
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);
|
Linking.openURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user