chore: add select

This commit is contained in:
2026-03-06 18:21:33 +01:00
parent a66f570df4
commit e14a1eb4b8
3 changed files with 347 additions and 237 deletions
+147 -74
View File
@@ -28,7 +28,6 @@ import type { AdminStackParamList } from "../../navigation/types";
import StatusBadge from "../../components/StatusBadge";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
import Modal from "../../components/ui/Modal";
import Button from "../../components/ui/Button";
import Card from "../../components/ui/Card";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
@@ -55,6 +54,7 @@ export default function OrdersScreen() {
const [filter, setFilter] = useState("all");
const { alert, showError, showSuccess, showConfirm, hideAlert } = useAlert();
const [openMenuId, setOpenMenuId] = useState<number | null>(null);
const [assignModal, setAssignModal] = useState<{
visible: boolean;
commandId: number | null;
@@ -243,11 +243,47 @@ export default function OrdersScreen() {
alignItems: "center",
marginBottom: spacing.s,
},
actions: {
selectBtn: {
flexDirection: "row",
flexWrap: "wrap",
gap: spacing.s,
alignItems: "center",
justifyContent: "space-between",
marginTop: spacing.m,
paddingHorizontal: spacing.m,
paddingVertical: spacing.s,
backgroundColor: colors.bgPrimary,
borderRadius: borderRadius.sm,
borderWidth: 1,
borderColor: colors.border,
},
selectBtnText: {
color: colors.textSecondary,
fontSize: fontSize.sm,
},
dropdown: {
marginTop: 2,
backgroundColor: colors.bgCard,
borderRadius: borderRadius.sm,
borderWidth: 1,
borderColor: colors.border,
overflow: "hidden",
},
dropdownItem: {
flexDirection: "row",
alignItems: "center",
gap: spacing.s,
paddingHorizontal: spacing.m,
paddingVertical: spacing.m,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
dropdownItemLast: { borderBottomWidth: 0 },
dropdownText: {
color: colors.textWhite,
fontSize: fontSize.sm,
},
dropdownTextDanger: {
color: colors.error,
fontSize: fontSize.sm,
},
empty: {
color: colors.textMuted,
@@ -313,80 +349,117 @@ export default function OrdersScreen() {
[colors],
);
const renderOrder = ({ item }: { item: CommandResponse }) => (
<Card style={{ marginBottom: spacing.m }}>
<TouchableOpacity
onPress={() =>
navigation.navigate("OrderDetail", { orderId: item.id })
}
activeOpacity={0.7}
>
<View style={styles.row}>
<Text style={styles.orderId}>#{item.id}</Text>
<StatusBadge status={item.status} />
</View>
<Text style={styles.cardText}>Client: {item.username}</Text>
<Text style={styles.cardText}>Adresse: {item.adresse}</Text>
<Text style={styles.cardText}>
Total: {item.total_prix.toFixed(2)}
</Text>
{item.livreur_assign && (
const renderOrder = ({ item }: { item: CommandResponse }) => {
const isOpen = openMenuId === item.id;
const isDone = ["approved", "cancelled"].includes(item.status);
type Action = {
label: string;
icon: keyof typeof Ionicons.glyphMap;
onPress: () => void;
danger?: boolean;
condition?: boolean;
};
const actions: Action[] = [
{
label: "Voir items",
icon: "receipt-outline",
onPress: () => { setOpenMenuId(null); openItems(item.id); },
},
{
label: "Le client est là",
icon: "notifications-outline",
onPress: () => { setOpenMenuId(null); handleNotifyClient(item.id); },
condition: !isDone,
},
{
label: "Assigner livreur",
icon: "bicycle-outline",
onPress: () => { setOpenMenuId(null); openAssignModal(item.id); },
condition: !isDone,
},
{
label: "Passer en route",
icon: "car-outline",
onPress: () => { setOpenMenuId(null); handleStatusUpdate(item.id, "en_route"); },
condition: item.status === "assigned",
},
{
label: "Confirmer réception",
icon: "checkmark-circle-outline",
onPress: () => { setOpenMenuId(null); handleConfirmReception(item.id); },
condition: item.status === "livre",
},
{
label: "Supprimer",
icon: "trash-outline",
onPress: () => { setOpenMenuId(null); handleDelete(item.id); },
danger: true,
},
].filter((a) => a.condition !== false);
return (
<Card style={{ marginBottom: spacing.m }}>
<TouchableOpacity
onPress={() => navigation.navigate("OrderDetail", { orderId: item.id })}
activeOpacity={0.7}
>
<View style={styles.row}>
<Text style={styles.orderId}>#{item.id}</Text>
<StatusBadge status={item.status} />
</View>
<Text style={styles.cardText}>Client: {item.username}</Text>
<Text style={styles.cardText}>Adresse: {item.adresse}</Text>
<Text style={styles.cardText}>
Livreur: {item.livreur_assign}
Total: {item.total_prix.toFixed(2)}
</Text>
)}
<Text style={styles.cardDate}>
{new Date(item.created_at).toLocaleString("fr-FR")}
</Text>
</TouchableOpacity>
<View style={styles.actions}>
<Button
title="Voir items"
onPress={() => openItems(item.id)}
size="sm"
variant="primary"
/>
{!["approved", "cancelled"].includes(item.status) && (
<Button
title="Le client est là"
onPress={() => handleNotifyClient(item.id)}
size="sm"
variant="warning"
{item.livreur_assign && (
<Text style={styles.cardText}>Livreur: {item.livreur_assign}</Text>
)}
<Text style={styles.cardDate}>
{new Date(item.created_at).toLocaleString("fr-FR")}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.selectBtn}
onPress={() => setOpenMenuId(isOpen ? null : item.id)}
>
<Text style={styles.selectBtnText}>Actions</Text>
<Ionicons
name={isOpen ? "chevron-up" : "chevron-down"}
size={14}
color={colors.textSecondary}
/>
</TouchableOpacity>
{isOpen && (
<View style={styles.dropdown}>
{actions.map((action, index) => (
<TouchableOpacity
key={action.label}
style={[
styles.dropdownItem,
index === actions.length - 1 && styles.dropdownItemLast,
]}
onPress={action.onPress}
>
<Ionicons
name={action.icon}
size={16}
color={action.danger ? colors.error : colors.accent}
/>
<Text style={action.danger ? styles.dropdownTextDanger : styles.dropdownText}>
{action.label}
</Text>
</TouchableOpacity>
))}
</View>
)}
{!["approved", "cancelled"].includes(item.status) && (
<Button
title="Assigner"
onPress={() => openAssignModal(item.id)}
size="sm"
variant="secondary"
/>
)}
{item.status === "assigned" && (
<Button
title="En route"
onPress={() => handleStatusUpdate(item.id, "en_route")}
size="sm"
variant="secondary"
/>
)}
{item.status === "livre" && (
<Button
title="Confirmer"
onPress={() => handleConfirmReception(item.id)}
size="sm"
variant="success"
/>
)}
<Button
title="Supprimer"
onPress={() => handleDelete(item.id)}
size="sm"
variant="danger"
/>
</View>
</Card>
);
</Card>
);
};
if (loading) return <LoadingSpinner message="Chargement commandes..." />;
+166 -149
View File
@@ -24,7 +24,6 @@ import type { CommandResponse } from "../../api/types";
import StatusBadge from "../../components/StatusBadge";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
import Modal from "../../components/ui/Modal";
import Button from "../../components/ui/Button";
import Card from "../../components/ui/Card";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
@@ -52,6 +51,7 @@ export default function OrdersScreen() {
const [commands, setCommands] = useState<CommandResponse[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [openMenuId, setOpenMenuId] = useState<number | null>(null);
const { alert, showError, showSuccess, showConfirm, hideAlert } = useAlert();
const [itemsModal, setItemsModal] = useState<{
visible: boolean;
@@ -105,6 +105,7 @@ export default function OrdersScreen() {
};
const openItems = async (commandId: number) => {
setOpenMenuId(null);
try {
const result = await getCommandItems(commandId);
setItemsModal({
@@ -119,8 +120,8 @@ export default function OrdersScreen() {
}
};
const handleConfirmReception = (commandId: number) => {
setOpenMenuId(null);
showConfirm(
"Confirmer la réception",
`Confirmer la réception de la commande #${commandId} au nom du client ?`,
@@ -137,6 +138,7 @@ export default function OrdersScreen() {
};
const handleNotifyClient = async (commandId: number) => {
setOpenMenuId(null);
try {
await notifyClientToDescendCabine(commandId);
showSuccess("Notification envoyée", "Le client a été prévenu de descendre");
@@ -146,6 +148,7 @@ export default function OrdersScreen() {
};
const handleDelete = (commandId: number) => {
setOpenMenuId(null);
showConfirm(
"Supprimer",
`Supprimer la commande #${commandId} ?`,
@@ -171,6 +174,7 @@ export default function OrdersScreen() {
});
const openAssignModal = async (commandId: number) => {
setOpenMenuId(null);
try {
const list = await getCabineLivreursList();
setLivreurs(list);
@@ -211,18 +215,56 @@ export default function OrdersScreen() {
fontSize: fontSize.sm,
marginTop: 2,
},
actions: {
flexDirection: "row",
flexWrap: "wrap",
gap: spacing.s,
marginTop: spacing.m,
},
empty: {
color: colors.textMuted,
textAlign: "center",
marginTop: spacing.xl,
},
// Select actions
selectBtn: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginTop: spacing.m,
paddingHorizontal: spacing.m,
paddingVertical: spacing.s,
backgroundColor: colors.bgPrimary,
borderRadius: borderRadius.sm,
borderWidth: 1,
borderColor: colors.border,
},
selectBtnText: {
color: colors.textSecondary,
fontSize: fontSize.sm,
},
dropdown: {
marginTop: 2,
backgroundColor: colors.bgCard,
borderRadius: borderRadius.sm,
borderWidth: 1,
borderColor: colors.border,
overflow: "hidden",
},
dropdownItem: {
flexDirection: "row",
alignItems: "center",
gap: spacing.s,
paddingHorizontal: spacing.m,
paddingVertical: spacing.m,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
dropdownItemLast: {
borderBottomWidth: 0,
},
dropdownText: {
color: colors.textWhite,
fontSize: fontSize.sm,
},
dropdownTextDanger: {
color: colors.error,
fontSize: fontSize.sm,
},
// Modal summary
modalSummary: {
backgroundColor: colors.bgCard,
@@ -247,11 +289,7 @@ export default function OrdersScreen() {
fontWeight: "700",
marginTop: spacing.xs,
},
// Modal progress bar
progressContainer: {
marginBottom: spacing.m,
},
progressContainer: { marginBottom: spacing.m },
progressLabel: {
color: colors.textMuted,
fontSize: fontSize.xs,
@@ -263,21 +301,14 @@ export default function OrdersScreen() {
borderRadius: 2,
overflow: "hidden",
},
progressFill: {
height: "100%",
borderRadius: 2,
},
// Item card
progressFill: { height: "100%", borderRadius: 2 },
itemCard: {
backgroundColor: colors.bgCard,
borderRadius: borderRadius.sm,
marginBottom: spacing.s,
overflow: "hidden",
},
itemCardAccent: {
height: 3,
},
itemCardAccent: { height: 3 },
itemCardBody: {
padding: spacing.m,
flexDirection: "row",
@@ -315,26 +346,7 @@ export default function OrdersScreen() {
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",
},
itemStatusText: { fontSize: fontSize.xs, fontWeight: "600" },
livreurItem: {
flexDirection: "row",
alignItems: "center",
@@ -349,57 +361,105 @@ export default function OrdersScreen() {
[colors],
);
const renderOrder = ({ item }: { item: CommandResponse }) => (
<Card style={{ marginBottom: spacing.m }}>
<View style={styles.row}>
<Text style={styles.orderId}>#{item.id}</Text>
<StatusBadge status={item.status} />
</View>
<Text style={styles.info}>Client: {item.username}</Text>
<Text style={styles.info}>Adresse: {item.adresse}</Text>
<Text style={styles.info}>
Total: {item.total_prix.toFixed(2)}
</Text>
<View style={styles.actions}>
<Button
title="Voir items"
onPress={() => openItems(item.id)}
size="sm"
variant="primary"
/>
<Button
title="Le client est là"
onPress={() => handleNotifyClient(item.id)}
size="sm"
variant="warning"
/>
<Button
title="Assigner"
onPress={() => openAssignModal(item.id)}
size="sm"
variant="secondary"
/>
{item.status === "livre" && (
<Button
title="Confirmer réception"
onPress={() => handleConfirmReception(item.id)}
size="sm"
variant="success"
/>
)}
<Button
title="Supprimer"
onPress={() => handleDelete(item.id)}
size="sm"
variant="danger"
/>
</View>
</Card>
);
const renderOrder = ({ item }: { item: CommandResponse }) => {
const isOpen = openMenuId === item.id;
const readyCount = itemsModal.items.filter(
(i) => i.status === "ready",
).length;
type Action = {
label: string;
icon: keyof typeof Ionicons.glyphMap;
onPress: () => void;
danger?: boolean;
condition?: boolean;
};
const actions: Action[] = [
{
label: "Voir items",
icon: "receipt-outline",
onPress: () => openItems(item.id),
},
{
label: "Le client est là",
icon: "notifications-outline",
onPress: () => handleNotifyClient(item.id),
},
{
label: "Assigner livreur",
icon: "bicycle-outline",
onPress: () => openAssignModal(item.id),
},
{
label: "Confirmer réception",
icon: "checkmark-circle-outline",
onPress: () => handleConfirmReception(item.id),
condition: item.status === "livre",
},
{
label: "Supprimer",
icon: "trash-outline",
onPress: () => handleDelete(item.id),
danger: true,
},
].filter((a) => a.condition !== false);
return (
<Card style={{ marginBottom: spacing.m }}>
<View style={styles.row}>
<Text style={styles.orderId}>#{item.id}</Text>
<StatusBadge status={item.status} />
</View>
<Text style={styles.info}>Client: {item.username}</Text>
<Text style={styles.info}>Adresse: {item.adresse}</Text>
<Text style={styles.info}>
Total: {item.total_prix.toFixed(2)}
</Text>
<TouchableOpacity
style={styles.selectBtn}
onPress={() => setOpenMenuId(isOpen ? null : item.id)}
>
<Text style={styles.selectBtnText}>Actions</Text>
<Ionicons
name={isOpen ? "chevron-up" : "chevron-down"}
size={14}
color={colors.textSecondary}
/>
</TouchableOpacity>
{isOpen && (
<View style={styles.dropdown}>
{actions.map((action, index) => (
<TouchableOpacity
key={action.label}
style={[
styles.dropdownItem,
index === actions.length - 1 && styles.dropdownItemLast,
]}
onPress={action.onPress}
>
<Ionicons
name={action.icon}
size={16}
color={action.danger ? colors.error : colors.accent}
/>
<Text
style={
action.danger
? styles.dropdownTextDanger
: styles.dropdownText
}
>
{action.label}
</Text>
</TouchableOpacity>
))}
</View>
)}
</Card>
);
};
const readyCount = itemsModal.items.filter((i) => i.status === "ready").length;
const totalCount = itemsModal.items.length;
const progress = totalCount > 0 ? readyCount / totalCount : 0;
@@ -430,20 +490,12 @@ export default function OrdersScreen() {
title={`Commande #${itemsModal.commandId}`}
icon="receipt-outline"
>
<ScrollView
showsVerticalScrollIndicator={false}
bounces={false}
>
{/* Résumé commande / client */}
<ScrollView showsVerticalScrollIndicator={false} bounces={false}>
{(itemsModal.commandInfo || itemsModal.clientInfo) && (
<View style={styles.modalSummary}>
{itemsModal.clientInfo?.username && (
<View style={styles.modalSummaryRow}>
<Ionicons
name="person-outline"
size={14}
color={colors.textMuted}
/>
<Ionicons name="person-outline" size={14} color={colors.textMuted} />
<Text style={styles.modalSummaryText}>
{itemsModal.clientInfo.prenom}{" "}
{itemsModal.clientInfo.nom} ·{" "}
@@ -453,11 +505,7 @@ export default function OrdersScreen() {
)}
{(itemsModal.commandInfo?.address || itemsModal.commandInfo?.adresse) && (
<View style={styles.modalSummaryRow}>
<Ionicons
name="location-outline"
size={14}
color={colors.textMuted}
/>
<Ionicons name="location-outline" size={14} color={colors.textMuted} />
<Text style={styles.modalSummaryText}>
{itemsModal.commandInfo.address || itemsModal.commandInfo.adresse}
</Text>
@@ -465,16 +513,12 @@ export default function OrdersScreen() {
)}
{itemsModal.commandInfo?.total_prix != null && (
<Text style={styles.modalTotal}>
{Number(
itemsModal.commandInfo.total_prix,
).toFixed(2)}{" "}
{Number(itemsModal.commandInfo.total_prix).toFixed(2)}
</Text>
)}
</View>
)}
{/* Barre de progression */}
{totalCount > 0 && (
<View style={styles.progressContainer}>
<Text style={styles.progressLabel}>
@@ -487,9 +531,7 @@ export default function OrdersScreen() {
{
width: `${progress * 100}%`,
backgroundColor:
progress === 1
? STATUS_COLORS.ready
: colors.info,
progress === 1 ? STATUS_COLORS.ready : colors.info,
},
]}
/>
@@ -497,23 +539,14 @@ export default function OrdersScreen() {
</View>
)}
{/* Liste des items */}
{itemsModal.items.map((item: any, index: number) => {
const statusColor =
STATUS_COLORS[item.status] || colors.textMuted;
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.itemCardAccent, { backgroundColor: statusColor }]} />
<View style={styles.itemCardBody}>
<View style={styles.itemIndex}>
<Text style={styles.itemIndexText}>
{index + 1}
</Text>
<Text style={styles.itemIndexText}>{index + 1}</Text>
</View>
<View style={styles.itemInfo}>
<Text style={styles.itemName}>
@@ -525,22 +558,12 @@ export default function OrdersScreen() {
</Text>
<View style={styles.itemStatusRow}>
<Ionicons
name={
STATUS_ICONS[
item.status
] || "ellipse-outline"
}
name={STATUS_ICONS[item.status] || "ellipse-outline"}
size={13}
color={statusColor}
/>
<Text
style={[
styles.itemStatusText,
{ color: statusColor },
]}
>
{STATUS_LABELS[item.status] ||
item.status}
<Text style={[styles.itemStatusText, { color: statusColor }]}>
{STATUS_LABELS[item.status] || item.status}
</Text>
</View>
</View>
@@ -557,9 +580,7 @@ export default function OrdersScreen() {
<Modal
visible={assignModal.visible}
onClose={() =>
setAssignModal({ visible: false, commandId: null })
}
onClose={() => setAssignModal({ visible: false, commandId: null })}
title={`Assigner commande #${assignModal.commandId}`}
icon="bicycle-outline"
>
@@ -569,11 +590,7 @@ export default function OrdersScreen() {
style={styles.livreurItem}
onPress={() => handleAssign(l.username)}
>
<Ionicons
name="person-outline"
size={20}
color={colors.accent}
/>
<Ionicons name="person-outline" size={20} color={colors.accent} />
<Text style={styles.livreurName}>{l.username}</Text>
</TouchableOpacity>
))}
@@ -103,7 +103,8 @@ export default function DashboardScreen() {
const pendingRouteAddress = useRef<string | null>(null);
const { alert, showError, showSuccess, hideAlert } = useAlert();
const [detailsDelivery, setDetailsDelivery] = useState<EnrichedDelivery | null>(null);
const [detailsDelivery, setDetailsDelivery] =
useState<EnrichedDelivery | null>(null);
const STATUS_COLORS: Record<string, string> = useMemo(
() => ({
@@ -235,7 +236,8 @@ export default function DashboardScreen() {
? `${client.prenom || ""} ${client.nom}`.trim()
: client?.username || undefined,
clientPhone: client?.telephone || undefined,
clientUsername: client?.username || undefined,
clientUsername:
client?.username || undefined,
clientNom: client?.nom || undefined,
clientPrenom: client?.prenom || undefined,
items:
@@ -320,7 +322,8 @@ export default function DashboardScreen() {
longitude: position.coords.longitude,
altitude: position.coords.altitude ?? 0,
accuracy: position.coords.accuracy ?? 0,
altitudeAccuracy: position.coords.altitudeAccuracy ?? 0,
altitudeAccuracy:
position.coords.altitudeAccuracy ?? 0,
heading: position.coords.heading ?? 0,
speed: position.coords.speed ?? 0,
},
@@ -329,7 +332,11 @@ export default function DashboardScreen() {
(error) => reject(new Error(error.message)),
// enableHighAccuracy: false → provider réseau/cell (1-3s)
// au lieu du GPS pur qui peut prendre 30-60s (cold start)
{ enableHighAccuracy: false, timeout: 15000, maximumAge: 10000 },
{
enableHighAccuracy: false,
timeout: 15000,
maximumAge: 10000,
},
);
}),
[],
@@ -516,9 +523,9 @@ export default function DashboardScreen() {
: item.status === "arrived"
? "Arrivé"
: item.status === "in_progress" ||
item.status === "en_route"
? "En cours"
: "En attente"
item.status === "en_route"
? "En cours"
: "En attente"
}
color={
item.status === "completed" ||
@@ -527,9 +534,9 @@ export default function DashboardScreen() {
: item.status === "arrived"
? colors.accent
: item.status === "in_progress" ||
item.status === "en_route"
? colors.warning
: colors.info
item.status === "en_route"
? colors.warning
: colors.info
}
/>
</View>
@@ -636,7 +643,11 @@ export default function DashboardScreen() {
onPress={() => setDetailsDelivery(item)}
activeOpacity={0.7}
>
<Ionicons name="list-outline" size={16} color={colors.accent} />
<Ionicons
name="list-outline"
size={16}
color={colors.accent}
/>
<Text style={styles.detailsBtnText}>Détails</Text>
</TouchableOpacity>
@@ -1578,7 +1589,11 @@ export default function DashboardScreen() {
{/* Infos client */}
<View style={styles.detailSection}>
<Text style={styles.detailSectionTitle}>
<Ionicons name="person-outline" size={14} color={colors.accent} />
<Ionicons
name="person-outline"
size={14}
color={colors.accent}
/>
{" "}Client
</Text>
<View style={styles.detailRow}>
@@ -1606,10 +1621,15 @@ export default function DashboardScreen() {
{/* Produits */}
<Text style={styles.detailSectionTitle}>
<Ionicons name="bag-outline" size={14} color={colors.success} />
<Ionicons
name="bag-outline"
size={14}
color={colors.success}
/>
{" "}Produits
</Text>
{detailsDelivery?.items && detailsDelivery.items.length > 0 ? (
{detailsDelivery?.items &&
detailsDelivery.items.length > 0 ? (
detailsDelivery.items.map((prod, idx) => (
<View key={idx} style={styles.detailProductRow}>
<View style={{ flex: 1 }}>