chore: build
Frontend Admin - EAS Build / build (push) Canceled after 0s
Frontend Client - EAS Build / build (push) Canceled after 0s
Backend - Build & Lint / build (push) Failing after 25m18s
Frontend Web - Build & Lint / build (push) Failing after 9m58s

This commit is contained in:
Xor290
2026-08-06 12:06:05 +02:00
parent c034088bee
commit 22a8d5026c
174 changed files with 30315 additions and 16120 deletions
@@ -20,6 +20,7 @@ import {
createCategoryAdmin,
updateCategoryAdmin,
deleteCategoryAdmin,
reorderCategoriesAdmin,
} from "../../api/api_admin";
import type { Category } from "../../api/api_admin";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
@@ -47,6 +48,7 @@ export default function CategoriesScreen() {
const [hexInput, setHexInput] = useState("#7c3aed");
const [isComingSoon, setIsComingSoon] = useState(false);
const [saving, setSaving] = useState(false);
const [reordering, setReordering] = useState(false);
const { alert, showError, showSuccess, showConfirm, hideAlert } = useAlert();
const load = useCallback(async () => {
@@ -144,6 +146,17 @@ export default function CategoriesScreen() {
);
};
const handleMove = async (index: number, direction: "up" | "down") => {
const newList = [...categories];
const swapIndex = direction === "up" ? index - 1 : index + 1;
if (swapIndex < 0 || swapIndex >= newList.length) return;
[newList[index], newList[swapIndex]] = [newList[swapIndex], newList[index]];
setCategories(newList);
setReordering(true);
await reorderCategoriesAdmin(newList.map((c) => c.id));
setReordering(false);
};
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bgPrimary },
header: {
@@ -189,6 +202,8 @@ export default function CategoriesScreen() {
paddingVertical: 2,
},
comingSoonBadgeText: { fontSize: fontSize.xs, color: colors.warning, fontWeight: "600" },
orderBtns: { flexDirection: "column", alignItems: "center", marginRight: spacing.s },
orderBtn: { padding: 2 },
actions: { flexDirection: "row", gap: spacing.s },
actionBtn: { padding: 8 },
overlay: {
@@ -297,9 +312,33 @@ export default function CategoriesScreen() {
ListEmptyComponent={
<Text style={styles.empty}>Aucune catégorie. Créez-en une !</Text>
}
renderItem={({ item }) => (
renderItem={({ item, index }) => (
<Card>
<View style={styles.row}>
<View style={styles.orderBtns}>
<TouchableOpacity
onPress={() => handleMove(index, "up")}
disabled={index === 0 || reordering}
style={styles.orderBtn}
>
<Ionicons
name="chevron-up"
size={18}
color={index === 0 ? colors.textMuted : colors.textSecondary}
/>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleMove(index, "down")}
disabled={index === categories.length - 1 || reordering}
style={styles.orderBtn}
>
<Ionicons
name="chevron-down"
size={18}
color={index === categories.length - 1 ? colors.textMuted : colors.textSecondary}
/>
</TouchableOpacity>
</View>
<View style={styles.catLeft}>
<View
style={[
@@ -15,6 +15,8 @@ import {
Modal,
StatusBar,
useWindowDimensions,
ScrollView,
Pressable,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { spacing, fontSize, borderRadius } from "../../theme";
@@ -22,7 +24,10 @@ import { useTheme } from "../../context/ThemeContext";
import {
getAllDeliveryPersonsWithDetails,
getCommandByID,
getLivreurRatings,
getLivreurLoginHistory,
} from "../../api/api_admin";
import type { LoginHistoryWeek } from "../../api/api_admin";
import { geocodeAddress, calculateRoute } from "../../api/tomtom";
import type { RouteInfo } from "../../api/tomtom";
import type { DeliveryPerson } from "../../api/types";
@@ -61,6 +66,76 @@ export default function DeliveryScreen() {
const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
const [routeLoading, setRouteLoading] = useState(false);
// Avis livreur
const [ratingsModal, setRatingsModal] = useState<{
username: string;
ratings: { id: number; order_id: number; client_username: string; rating: number; comment: string; created_at: string }[];
average: number;
count: number;
} | null>(null);
const [ratingsLoading, setRatingsLoading] = useState(false);
const openRatings = async (username: string) => {
setRatingsLoading(true);
const data = await getLivreurRatings(username);
setRatingsModal({ username, ...data });
setRatingsLoading(false);
};
// Historique de connexion livreur
const [loginHistoryModal, setLoginHistoryModal] = useState<{
username: string;
year: number;
month: number;
weeks: LoginHistoryWeek[];
} | null>(null);
const [loginHistoryLoading, setLoginHistoryLoading] = useState(false);
const loginHistoryRequestRef = useRef(0);
const fetchLoginHistory = async (
username: string,
year: number,
month: number,
) => {
const requestId = ++loginHistoryRequestRef.current;
setLoginHistoryLoading(true);
const res = await getLivreurLoginHistory(username, year, month);
if (requestId !== loginHistoryRequestRef.current) return;
setLoginHistoryModal({
username,
year: res.year,
month: res.month,
weeks: res.weeks,
});
setLoginHistoryLoading(false);
};
const openLoginHistory = (username: string) => {
const now = new Date();
fetchLoginHistory(username, now.getFullYear(), now.getMonth() + 1);
};
const changeLoginHistoryMonth = (delta: number) => {
if (!loginHistoryModal || loginHistoryLoading) return;
let year = loginHistoryModal.year;
let month = loginHistoryModal.month + delta;
if (month < 1) {
month = 12;
year -= 1;
} else if (month > 12) {
month = 1;
year += 1;
}
const now = new Date();
if (
year > now.getFullYear() ||
(year === now.getFullYear() && month > now.getMonth() + 1)
) {
return;
}
fetchLoginHistory(loginHistoryModal.username, year, month);
};
const loadData = useCallback(async () => {
try {
const result = await getAllDeliveryPersonsWithDetails();
@@ -338,6 +413,66 @@ export default function DeliveryScreen() {
fontWeight: "500",
},
ratingsBtn: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: spacing.s,
marginTop: spacing.m,
paddingVertical: spacing.s,
paddingHorizontal: spacing.m,
borderRadius: borderRadius.sm,
borderWidth: 1,
borderColor: "#f59e0b",
},
ratingsBtnText: {
color: "#f59e0b",
fontSize: fontSize.sm,
fontWeight: "600",
},
historyBtn: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: spacing.s,
marginTop: spacing.m,
paddingVertical: spacing.s,
paddingHorizontal: spacing.m,
borderRadius: borderRadius.sm,
borderWidth: 1,
borderColor: colors.accent,
},
historyBtnText: {
color: colors.accent,
fontSize: fontSize.sm,
fontWeight: "600",
},
historyWeekLabel: {
color: colors.textMuted,
fontSize: fontSize.xs,
fontWeight: "700",
textTransform: "uppercase",
marginBottom: spacing.xs,
marginTop: spacing.m,
},
historyEntryRow: {
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: spacing.xs,
borderTopWidth: 1,
borderTopColor: colors.borderLight,
},
historyEntryDate: {
color: colors.textPrimary,
fontSize: fontSize.sm,
},
historyEntryTime: {
color: colors.textMuted,
fontSize: fontSize.sm,
},
trackBtn: {
flexDirection: "row",
alignItems: "center",
@@ -356,6 +491,80 @@ export default function DeliveryScreen() {
fontWeight: "600",
},
ratingsOverlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.7)",
justifyContent: "flex-end",
},
ratingsSheet: {
backgroundColor: colors.bgCard,
borderTopLeftRadius: borderRadius.xl,
borderTopRightRadius: borderRadius.xl,
padding: spacing.l,
maxHeight: "80%",
},
ratingsHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: spacing.s,
},
ratingsTitle: {
color: colors.textWhite,
fontSize: fontSize.lg,
fontWeight: "700",
},
ratingsAvg: {
flexDirection: "row",
alignItems: "center",
gap: spacing.xs,
marginBottom: spacing.l,
},
ratingsAvgText: {
color: "#f59e0b",
fontSize: fontSize.md,
fontWeight: "700",
},
ratingsCount: {
color: colors.textMuted,
fontSize: fontSize.sm,
},
ratingItem: {
borderTopWidth: 1,
borderTopColor: colors.borderLight,
paddingVertical: spacing.m,
},
ratingItemHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: spacing.xs,
},
ratingItemClient: {
color: colors.textPrimary,
fontSize: fontSize.sm,
fontWeight: "600",
},
ratingItemDate: {
color: colors.textMuted,
fontSize: fontSize.xs,
},
ratingStarsRow: {
flexDirection: "row",
gap: 2,
marginBottom: spacing.xs,
},
ratingItemComment: {
color: colors.textSecondary,
fontSize: fontSize.sm,
fontStyle: "italic",
},
ratingsEmpty: {
color: colors.textMuted,
textAlign: "center",
paddingVertical: spacing.xl,
},
empty: {
color: colors.textMuted,
textAlign: "center",
@@ -523,6 +732,30 @@ export default function DeliveryScreen() {
</Text>
)}
<TouchableOpacity
style={styles.ratingsBtn}
onPress={() => openRatings(item.username)}
activeOpacity={0.7}
>
<Ionicons name="star-outline" size={14} color="#f59e0b" />
<Text style={styles.ratingsBtnText}>Voir les avis</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.historyBtn}
onPress={() => openLoginHistory(item.username)}
activeOpacity={0.7}
>
<Ionicons
name="time-outline"
size={14}
color={colors.accent}
/>
<Text style={styles.historyBtnText}>
Historique de connexion
</Text>
</TouchableOpacity>
{hasGPS && (
<TouchableOpacity
style={[
@@ -835,6 +1068,252 @@ export default function DeliveryScreen() {
<Text style={styles.empty}>Aucun livreur</Text>
}
/>
{/* ── Modal avis livreur ── */}
<Modal
visible={ratingsModal !== null}
transparent
animationType="slide"
onRequestClose={() => setRatingsModal(null)}
>
<Pressable style={styles.ratingsOverlay} onPress={() => setRatingsModal(null)}>
<Pressable onPress={() => {}}>
<View style={styles.ratingsSheet}>
<View style={styles.ratingsHeader}>
<Text style={styles.ratingsTitle}>
Avis — {ratingsModal?.username}
</Text>
<TouchableOpacity onPress={() => setRatingsModal(null)}>
<Ionicons name="close" size={22} color={colors.textMuted} />
</TouchableOpacity>
</View>
{ratingsLoading ? (
<Text style={styles.ratingsEmpty}>Chargement...</Text>
) : ratingsModal && ratingsModal.count > 0 ? (
<>
<View style={styles.ratingsAvg}>
{[1,2,3,4,5].map((s) => (
<Ionicons
key={s}
name={s <= Math.round(ratingsModal.average) ? "star" : "star-outline"}
size={20}
color="#f59e0b"
/>
))}
<Text style={styles.ratingsAvgText}>
{ratingsModal.average.toFixed(1)}
</Text>
<Text style={styles.ratingsCount}>
({ratingsModal.count} avis)
</Text>
</View>
<ScrollView showsVerticalScrollIndicator={false}>
{ratingsModal.ratings.map((r) => (
<View key={r.id} style={styles.ratingItem}>
<View style={styles.ratingItemHeader}>
<Text style={styles.ratingItemClient}>{r.client_username}</Text>
<Text style={styles.ratingItemDate}>
{new Date(r.created_at).toLocaleDateString("fr-FR")}
</Text>
</View>
<View style={styles.ratingStarsRow}>
{[1,2,3,4,5].map((s) => (
<Ionicons
key={s}
name={s <= r.rating ? "star" : "star-outline"}
size={14}
color="#f59e0b"
/>
))}
</View>
{r.comment !== "" && (
<Text style={styles.ratingItemComment}>"{r.comment}"</Text>
)}
</View>
))}
</ScrollView>
</>
) : (
<Text style={styles.ratingsEmpty}>Aucun avis pour ce livreur</Text>
)}
</View>
</Pressable>
</Pressable>
</Modal>
{/* ── Modal historique de connexion livreur ── */}
<Modal
visible={loginHistoryModal !== null}
transparent
animationType="slide"
onRequestClose={() => setLoginHistoryModal(null)}
>
<Pressable
style={styles.ratingsOverlay}
onPress={() => setLoginHistoryModal(null)}
>
<Pressable onPress={() => {}}>
<View style={styles.ratingsSheet}>
<View style={styles.ratingsHeader}>
<Text style={styles.ratingsTitle}>
Connexions — {loginHistoryModal?.username}
</Text>
<TouchableOpacity
onPress={() => setLoginHistoryModal(null)}
>
<Ionicons
name="close"
size={22}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
{(() => {
const now = new Date();
const isCurrentMonth =
!!loginHistoryModal &&
loginHistoryModal.year ===
now.getFullYear() &&
loginHistoryModal.month ===
now.getMonth() + 1;
const canGoBack = !loginHistoryLoading;
const canGoForward =
!loginHistoryLoading && !isCurrentMonth;
return (
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginBottom: spacing.m,
}}
>
<TouchableOpacity
disabled={!canGoBack}
onPress={() =>
changeLoginHistoryMonth(-1)
}
hitSlop={8}
>
<Ionicons
name="chevron-back"
size={20}
color={
canGoBack
? colors.textPrimary
: colors.textMuted
}
/>
</TouchableOpacity>
<Text
style={{
color: colors.textWhite,
fontWeight: "700",
fontSize: fontSize.md,
}}
>
{loginHistoryModal &&
new Date(
loginHistoryModal.year,
loginHistoryModal.month -
1,
1,
)
.toLocaleDateString(
"fr-FR",
{
month: "long",
year: "numeric",
},
)
.replace(/^./, (c) =>
c.toUpperCase(),
)}
</Text>
<TouchableOpacity
disabled={!canGoForward}
onPress={() =>
changeLoginHistoryMonth(1)
}
hitSlop={8}
>
<Ionicons
name="chevron-forward"
size={20}
color={
canGoForward
? colors.textPrimary
: colors.textMuted
}
/>
</TouchableOpacity>
</View>
);
})()}
{loginHistoryLoading ? (
<Text style={styles.ratingsEmpty}>
Chargement...
</Text>
) : loginHistoryModal &&
loginHistoryModal.weeks.length > 0 ? (
<ScrollView showsVerticalScrollIndicator={false}>
{loginHistoryModal.weeks.map((week) => (
<View key={week.week}>
<Text style={styles.historyWeekLabel}>
Semaine {week.week}
</Text>
{week.entries.map((entry) => (
<View
key={entry.id}
style={styles.historyEntryRow}
>
<Text
style={
styles.historyEntryDate
}
>
{new Date(
entry.created_at,
).toLocaleDateString(
"fr-FR",
{
weekday: "short",
day: "2-digit",
month: "2-digit",
},
)}
</Text>
<Text
style={
styles.historyEntryTime
}
>
{new Date(
entry.created_at,
).toLocaleTimeString(
"fr-FR",
{
hour: "2-digit",
minute: "2-digit",
},
)}
</Text>
</View>
))}
</View>
))}
</ScrollView>
) : (
<Text style={styles.ratingsEmpty}>
Aucune connexion ce mois-ci
</Text>
)}
</View>
</Pressable>
</Pressable>
</Modal>
</View>
);
}
@@ -782,7 +782,7 @@ export default function OrdersScreen() {
setOpenMenuId(null);
handleForceValidate(item.id);
},
condition: !isDone && item.status !== "livre",
condition: !isDone,
},
{
label: "Supprimer",
@@ -62,12 +62,12 @@ interface PendingMedia {
// Les catégories sont chargées dynamiquement depuis l'API
const UNITS = [
{ value: "u", label: "u" },
{ value: "kg", label: "kg" },
{ value: "g", label: "g" },
{ value: "u", label: "u" },
{ value: "kg", label: "kg" },
{ value: "g", label: "g" },
{ value: "bag", label: "bag" },
{ value: "l", label: "l" },
{ value: "cl", label: "cl" },
{ value: "l", label: "l" },
{ value: "cl", label: "cl" },
{ value: "pcs", label: "pcs" },
];
@@ -247,7 +247,11 @@ export default function ProductsScreen() {
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: type === "image" ? ["images"] : ["videos"],
mediaTypes:
type === "image"
? ImagePicker.MediaTypeOptions.Images
: ImagePicker.MediaTypeOptions.Videos,
quality: 0.8,
allowsMultipleSelection: true,
});
@@ -354,10 +358,7 @@ export default function ProductsScreen() {
prices.forEach((p, i) => {
fd.append(`prices[${i}][quantity]`, String(p.quantity));
fd.append(`prices[${i}][price]`, String(p.price));
fd.append(
`prices[${i}][active_price]`,
p.active_price ? "true" : "false",
);
fd.append(`prices[${i}][active_price]`, p.active_price ? "true" : "false");
});
// Attacher les médias en attente
@@ -387,18 +388,13 @@ export default function ProductsScreen() {
m.mediaType,
);
} catch (uploadErr: any) {
const msg =
uploadErr?.response?.data?.error ||
uploadErr?.message ||
`Erreur upload ${m.mediaType}`;
const msg = uploadErr?.response?.data?.error || uploadErr?.message || `Erreur upload ${m.mediaType}`;
uploadErrors.push(msg);
}
}
setUploadingMedia(false);
if (uploadErrors.length > 0) {
setFormError(
`Erreur upload média: ${uploadErrors.join(", ")}`,
);
setFormError(`Erreur upload média: ${uploadErrors.join(", ")}`);
await loadData();
return;
}
@@ -649,14 +645,8 @@ export default function ProductsScreen() {
borderColor: "#22c55e",
backgroundColor: "#22c55e20",
},
comingSoonBtnText: {
color: colors.textMuted,
fontSize: fontSize.sm,
},
comingSoonBtnTextActive: {
color: "#22c55e",
fontWeight: "700",
},
comingSoonBtnText: { color: colors.textMuted, fontSize: fontSize.sm },
comingSoonBtnTextActive: { color: "#22c55e", fontWeight: "700" },
// Prices
sectionHeader: {
@@ -818,10 +808,7 @@ export default function ProductsScreen() {
<Text style={styles.name}>{item.name}</Text>
<Badge
label={item.category}
color={
categories.find((c) => c.name === item.category)
?.color || colors.accent
}
color={categories.find(c => c.name === item.category)?.color || colors.accent}
/>
</View>
</View>
@@ -856,18 +843,9 @@ export default function ProductsScreen() {
)}
</View>
)}
<Text style={styles.info}>
Stock: {item.stock} {item.unit || "u"}
</Text>
<Text style={styles.info}>Stock: {item.stock} {item.unit || "u"}</Text>
{item.prices && item.prices.length > 0 && (
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
gap: 4,
marginTop: 2,
}}
>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 4, marginTop: 2 }}>
{item.prices.map((p, i) => (
<Text
key={i}
@@ -880,8 +858,7 @@ export default function ProductsScreen() {
},
]}
>
{p.quantity}
{item.unit || "u"} = {p.price}
{p.quantity}{item.unit || "u"} = {p.price}
{i < item.prices!.length - 1 ? " |" : ""}
</Text>
))}
@@ -1047,8 +1024,7 @@ export default function ProductsScreen() {
<TouchableOpacity
style={[
styles.comingSoonBtn,
form.comingSoon &&
styles.comingSoonBtnActive,
form.comingSoon && styles.comingSoonBtnActive,
]}
onPress={() =>
setForm((f) => {
@@ -1056,24 +1032,16 @@ export default function ProductsScreen() {
return {
...f,
comingSoon: next,
prices: f.prices.map((p) => ({
...p,
active: !next,
})),
prices: f.prices.map((p) => ({ ...p, active: !next })),
};
})
}
>
<Text
style={[
styles.comingSoonBtnText,
form.comingSoon &&
styles.comingSoonBtnTextActive,
]}
>
{form.comingSoon
? "À venir (activé)"
: "Marquer comme «À venir»"}
<Text style={[
styles.comingSoonBtnText,
form.comingSoon && styles.comingSoonBtnTextActive,
]}>
{form.comingSoon ? "À venir (activé)" : "Marquer comme «À venir»"}
</Text>
</TouchableOpacity>
@@ -1172,18 +1140,9 @@ export default function ProductsScreen() {
onPress={() => togglePriceActive(idx)}
>
<Ionicons
name={
p.active
? "checkmark-circle"
: "close-circle"
}
name={p.active ? "checkmark-circle" : "close-circle"}
size={22}
color={
p.active
? colors.success ||
"#22c55e"
: colors.danger
}
color={p.active ? colors.success || "#22c55e" : colors.danger}
/>
</TouchableOpacity>
{form.prices.length > 1 && (
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback, useRef } from "react";
import { useFocusEffect } from "@react-navigation/native";
import {
View,
Text,
@@ -1155,9 +1156,25 @@ function CentralRewardSection({
);
}
// Palette violette d'origine de l'application (thème par défaut historique)
const ORIGINAL_THEME_COLORS = {
admin_color_primary: "#7c3aed",
admin_color_secondary: "#000000",
admin_color_success: "#4ade80",
admin_color_danger: "#ef4444",
admin_color_warning: "#f59e0b",
client_color_primary: "#7c3aed",
client_color_secondary: "#000000",
client_color_success: "#4ade80",
client_color_danger: "#ef4444",
client_color_warning: "#f59e0b",
client_title_gradient_from: "#a78bfa",
client_title_gradient_to: "#22d3ee",
} as const;
export default function SettingsScreen() {
const { colors } = useTheme();
const { alert, showError, showSuccess, hideAlert } = useAlert();
const { colors, refreshColors } = useTheme();
const { alert, showError, showSuccess, showConfirm, hideAlert } = useAlert();
const navigation = useNavigation();
const { width: screenWidth } = useWindowDimensions();
const inputMd = screenWidth < 380 ? 64 : 80;
@@ -1194,6 +1211,18 @@ export default function SettingsScreen() {
shop_name: "Milieu-Nantais",
contact_telegram: "",
points_reward: null,
admin_color_primary: "#7c3aed",
admin_color_secondary: "#22d3ee",
admin_color_success: "#4ade80",
admin_color_danger: "#ef4444",
admin_color_warning: "#f59e0b",
client_color_primary: "#7c3aed",
client_color_secondary: "#22d3ee",
client_color_success: "#4ade80",
client_color_danger: "#ef4444",
client_color_warning: "#f59e0b",
client_title_gradient_from: "#a78bfa",
client_title_gradient_to: "#22d3ee",
});
const [showApiKey, setShowApiKey] = useState(false);
const [showIpnSecret, setShowIpnSecret] = useState(false);
@@ -1280,9 +1309,13 @@ export default function SettingsScreen() {
}
}, []);
useEffect(() => {
loadData();
}, [loadData]);
useFocusEffect(
useCallback(() => {
if (!isDirty.current) {
loadData();
}
}, [loadData])
);
// Retourne l'index du pool auquel la catégorie est assignée, ou -1 si aucun
const getPoolIndexFor = (catName: string): number => {
@@ -1356,6 +1389,8 @@ export default function SettingsScreen() {
setSaving(false);
if (res.success) {
isDirty.current = false;
await loadData();
await refreshColors();
showSuccess("Succès", "Paramètres sauvegardés");
} else {
showError("Erreur", res.error || "Erreur lors de la sauvegarde");
@@ -1484,6 +1519,7 @@ export default function SettingsScreen() {
<ScrollView contentContainerStyle={s.content}>
{/* Personnalisation */}
<AccordionSection title="Personnalisation" colors={colors} s={s}>
{/* Nom du shop */}
<View style={[s.row, s.rowFirst]}>
<View style={s.rowLeft}>
<Text style={s.rowLabel}>Nom du shop</Text>
@@ -1500,6 +1536,67 @@ export default function SettingsScreen() {
autoCorrect={false}
/>
</View>
{/* Dégradé titre boutique */}
<View style={{ borderTopWidth: 1, borderTopColor: colors.border, paddingHorizontal: spacing.l, paddingVertical: spacing.m, gap: spacing.m }}>
<Text style={s.rowLabel}>Dégradé du titre boutique</Text>
<Text style={s.rowDesc}>Couleurs du nom de la boutique dans le header du site client.</Text>
{/* Aperçu du dégradé */}
<View style={{ height: 36, borderRadius: borderRadius.sm, overflow: "hidden" }}>
<View style={{
flex: 1,
backgroundColor: settings.client_title_gradient_from,
// gradient simulé : deux moitiés de couleur
}}>
<View style={{
position: "absolute", right: 0, top: 0, bottom: 0,
width: "50%",
backgroundColor: settings.client_title_gradient_to,
}} />
<View style={{
position: "absolute", left: "25%", right: "25%", top: 0, bottom: 0,
backgroundColor: `${settings.client_title_gradient_from}00`,
}} />
</View>
</View>
{/* Couleur de départ */}
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.m }}>
<View style={{ width: 32, height: 32, borderRadius: borderRadius.sm, backgroundColor: settings.client_title_gradient_from, borderWidth: 1, borderColor: colors.border }} />
<View style={{ flex: 1 }}>
<Text style={[s.rowDesc, { marginBottom: 4 }]}>Couleur de départ</Text>
<TextInput
style={[s.input, { fontFamily: "monospace" }]}
value={settings.client_title_gradient_from}
onChangeText={(v) => setSettings((p) => ({ ...p, client_title_gradient_from: v }))}
placeholder="#a78bfa"
placeholderTextColor={colors.textMuted}
autoCorrect={false}
autoCapitalize="none"
maxLength={7}
/>
</View>
</View>
{/* Couleur de fin */}
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.m }}>
<View style={{ width: 32, height: 32, borderRadius: borderRadius.sm, backgroundColor: settings.client_title_gradient_to, borderWidth: 1, borderColor: colors.border }} />
<View style={{ flex: 1 }}>
<Text style={[s.rowDesc, { marginBottom: 4 }]}>Couleur de fin</Text>
<TextInput
style={[s.input, { fontFamily: "monospace" }]}
value={settings.client_title_gradient_to}
onChangeText={(v) => setSettings((p) => ({ ...p, client_title_gradient_to: v }))}
placeholder="#22d3ee"
placeholderTextColor={colors.textMuted}
autoCorrect={false}
autoCapitalize="none"
maxLength={7}
/>
</View>
</View>
</View>
</AccordionSection>
{/* Amendes */}
@@ -2182,6 +2279,97 @@ export default function SettingsScreen() {
)}
</AccordionSection>
{/* ============================================ */}
{/* 🎨 COULEURS DE L'INTERFACE */}
{/* ============================================ */}
<AccordionSection title="Couleurs de l'interface" colors={colors} s={s}>
<Text style={[s.hint, { paddingTop: spacing.s, paddingHorizontal: spacing.l }]}>
Personnalisez les couleurs de l'espace admin et de l'app client / site web.
</Text>
<TouchableOpacity
onPress={() => showConfirm(
"Restaurer le thème d'origine",
"Toutes les couleurs personnalisées seront remplacées par le violet de base des premières versions de l'application. Continuer ?",
() => {
Keyboard.dismiss();
setSettings((p) => ({ ...p, ...ORIGINAL_THEME_COLORS }));
},
"Restaurer",
"Annuler",
)}
style={{
flexDirection: "row",
alignItems: "center",
gap: spacing.xs,
marginHorizontal: spacing.l,
marginTop: spacing.m,
paddingVertical: spacing.s,
paddingHorizontal: spacing.m,
borderRadius: borderRadius.sm,
borderWidth: 1,
borderColor: "#7c3aed",
alignSelf: "flex-start",
}}
>
<Ionicons name="color-palette-outline" size={16} color="#7c3aed" />
<Text style={{ fontSize: fontSize.sm, color: "#7c3aed", fontWeight: "600" }}>
Restaurer les couleurs de base
</Text>
</TouchableOpacity>
{(["admin", "client"] as const).map((scope) => (
<View key={scope}>
<Text style={[s.rowLabel, { paddingHorizontal: spacing.l, paddingTop: spacing.l, paddingBottom: spacing.xs }]}>
{scope === "admin" ? "Espace admin" : "App client & site web"}
</Text>
{([
{ label: "Principale", key: `${scope}_color_primary` as const },
{ label: "Secondaire", key: `${scope}_color_secondary` as const },
{ label: "Succès", key: `${scope}_color_success` as const },
{ label: "Danger", key: `${scope}_color_danger` as const },
{ label: "Avertissement", key: `${scope}_color_warning` as const },
] as { label: string; key: keyof typeof settings }[]).map(({ label, key }) => (
<View key={key as string} style={{ paddingHorizontal: spacing.l, paddingBottom: spacing.m }}>
<Text style={[s.rowDesc, { marginBottom: spacing.xs }]}>{label}</Text>
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.m }}>
<View style={{
width: 36, height: 36,
borderRadius: borderRadius.sm,
backgroundColor: (settings[key] as string) || "#7c3aed",
borderWidth: 1,
borderColor: colors.border,
}} />
<TextInput
style={[s.input, { flex: 1 }]}
value={settings[key] as string}
onChangeText={(v) => setSettings((p) => ({ ...p, [key]: v }))}
placeholder="#7c3aed"
placeholderTextColor={colors.textMuted}
autoCapitalize="none"
autoCorrect={false}
/>
</View>
<View style={{ flexDirection: "row", gap: spacing.xs, marginTop: spacing.xs, flexWrap: "wrap" }}>
{["#7c3aed", "#2563eb", "#0891b2", "#059669", "#d97706", "#dc2626", "#db2777", "#f59e0b", "#4ade80", "#ef4444", "#22d3ee", "#0f172a"].map((color) => (
<TouchableOpacity
key={color}
onPress={() => setSettings((p) => ({ ...p, [key]: color }))}
style={{
width: 26, height: 26, borderRadius: 13,
backgroundColor: color,
borderWidth: (settings[key] as string) === color ? 2.5 : 1,
borderColor: (settings[key] as string) === color ? colors.textPrimary : colors.border,
}}
/>
))}
</View>
</View>
))}
</View>
))}
</AccordionSection>
<TouchableOpacity
style={s.saveButton}
onPress={handleSave}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff