chore: build
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"package": "com.uberstup.adminpanel",
|
"package": "com.uberstup.adminpanel",
|
||||||
|
"abiFilters": ["arm64-v8a"],
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/icon.png",
|
"foregroundImage": "./assets/icon.png",
|
||||||
"backgroundColor": "#000000"
|
"backgroundColor": "#000000"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"buildType": "apk"
|
"buildType": "apk"
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"EXPO_PUBLIC_API_URL": "https://5.181.0.112.nip.io",
|
"EXPO_PUBLIC_API_URL": "https://uber-demo.club",
|
||||||
"EXPO_PUBLIC_UPDATE_URL": "https://ota.uber-stup.club"
|
"EXPO_PUBLIC_UPDATE_URL": "https://ota.uber-stup.club"
|
||||||
},
|
},
|
||||||
"channel": "pre-prod-admin"
|
"channel": "pre-prod-admin"
|
||||||
|
|||||||
Generated
+4
-18
@@ -80,7 +80,6 @@
|
|||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
|
||||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.28.6",
|
"@babel/code-frame": "^7.28.6",
|
||||||
"@babel/generator": "^7.28.6",
|
"@babel/generator": "^7.28.6",
|
||||||
@@ -1368,7 +1367,6 @@
|
|||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
@@ -2561,7 +2559,6 @@
|
|||||||
"version": "7.1.28",
|
"version": "7.1.28",
|
||||||
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.28.tgz",
|
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.28.tgz",
|
||||||
"integrity": "sha512-d1QDn+KNHfHGt3UIwOZvupvdsDdiHYZBEj7+wL2yDVo3tMezamYy60H9s3EnNVE1Ae1ty0trc7F2OKqo/RmsdQ==",
|
"integrity": "sha512-d1QDn+KNHfHGt3UIwOZvupvdsDdiHYZBEj7+wL2yDVo3tMezamYy60H9s3EnNVE1Ae1ty0trc7F2OKqo/RmsdQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-navigation/core": "^7.14.0",
|
"@react-navigation/core": "^7.14.0",
|
||||||
"escape-string-regexp": "^4.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
@@ -2761,7 +2758,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
|
||||||
"integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
|
"integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -3330,7 +3326,6 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -3976,6 +3971,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"iconv-lite": "^0.6.2"
|
"iconv-lite": "^0.6.2"
|
||||||
}
|
}
|
||||||
@@ -3985,6 +3981,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
},
|
},
|
||||||
@@ -4116,7 +4113,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.34.tgz",
|
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.34.tgz",
|
||||||
"integrity": "sha512-XkVHguZZDC8BcTQxHAd14/TQFbDp1Wt0Z/KApO9t68Ll5A127hLCPzU+a9gytfCIiyL/V1IpF1vIcOLKEVAoNQ==",
|
"integrity": "sha512-XkVHguZZDC8BcTQxHAd14/TQFbDp1Wt0Z/KApO9t68Ll5A127hLCPzU+a9gytfCIiyL/V1IpF1vIcOLKEVAoNQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.0",
|
"@babel/runtime": "^7.20.0",
|
||||||
"@expo/cli": "54.0.24",
|
"@expo/cli": "54.0.24",
|
||||||
@@ -4235,7 +4231,6 @@
|
|||||||
"version": "14.0.11",
|
"version": "14.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz",
|
||||||
"integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==",
|
"integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fontfaceobserver": "^2.1.0"
|
"fontfaceobserver": "^2.1.0"
|
||||||
},
|
},
|
||||||
@@ -7092,7 +7087,6 @@
|
|||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -7110,7 +7104,6 @@
|
|||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
@@ -7138,7 +7131,6 @@
|
|||||||
"version": "0.81.5",
|
"version": "0.81.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz",
|
||||||
"integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==",
|
"integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/create-cache-key-function": "^29.7.0",
|
"@jest/create-cache-key-function": "^29.7.0",
|
||||||
"@react-native/assets-registry": "0.81.5",
|
"@react-native/assets-registry": "0.81.5",
|
||||||
@@ -7265,7 +7257,6 @@
|
|||||||
"version": "5.6.2",
|
"version": "5.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
|
||||||
"integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
|
"integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*",
|
"react": "*",
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
@@ -7275,7 +7266,6 @@
|
|||||||
"version": "4.16.0",
|
"version": "4.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
|
||||||
"integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
|
"integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-freeze": "^1.0.0",
|
"react-freeze": "^1.0.0",
|
||||||
"react-native-is-edge-to-edge": "^1.2.1",
|
"react-native-is-edge-to-edge": "^1.2.1",
|
||||||
@@ -7290,7 +7280,6 @@
|
|||||||
"version": "0.21.2",
|
"version": "0.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
|
||||||
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
|
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.6",
|
"@babel/runtime": "^7.18.6",
|
||||||
"@react-native/normalize-colors": "^0.74.1",
|
"@react-native/normalize-colors": "^0.74.1",
|
||||||
@@ -7320,7 +7309,6 @@
|
|||||||
"version": "13.15.0",
|
"version": "13.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.15.0.tgz",
|
||||||
"integrity": "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==",
|
"integrity": "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"escape-string-regexp": "^4.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
"invariant": "2.2.4"
|
"invariant": "2.2.4"
|
||||||
@@ -7334,7 +7322,6 @@
|
|||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
|
||||||
"integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==",
|
"integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
|
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
|
||||||
"@babel/plugin-transform-class-properties": "^7.0.0-0",
|
"@babel/plugin-transform-class-properties": "^7.0.0-0",
|
||||||
@@ -7458,7 +7445,6 @@
|
|||||||
"version": "0.14.2",
|
"version": "0.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||||
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
|
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -7715,7 +7701,8 @@
|
|||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"optional": true
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/sax": {
|
"node_modules/sax": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
@@ -8321,7 +8308,6 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -151,9 +151,6 @@ export const updateUserByAdmin = async (
|
|||||||
return { success: true, message: data.message, user: data.user };
|
return { success: true, message: data.message, user: data.user };
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// COMMANDES
|
|
||||||
|
|
||||||
export const getAllCommands = async (status?: string, username?: string) => {
|
export const getAllCommands = async (status?: string, username?: string) => {
|
||||||
let url = `${V2}/admin/protected/orders`;
|
let url = `${V2}/admin/protected/orders`;
|
||||||
const params: string[] = [];
|
const params: string[] = [];
|
||||||
@@ -276,8 +273,6 @@ export const confirmReceptionAdmin = async (commandId: number) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// LIVREURS
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
export const getAvailableDeliveryPersons = async () => {
|
export const getAvailableDeliveryPersons = async () => {
|
||||||
@@ -930,6 +925,20 @@ export interface PointsTier {
|
|||||||
points: number;
|
points: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RewardCategoryConfig {
|
||||||
|
category: string;
|
||||||
|
all_products: boolean;
|
||||||
|
product_ids: number[];
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PointsReward {
|
||||||
|
threshold: number;
|
||||||
|
type: "free_product" | "half_price_product" | "custom";
|
||||||
|
description: string;
|
||||||
|
category_configs: RewardCategoryConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface PointsPool {
|
export interface PointsPool {
|
||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -1028,6 +1037,7 @@ export interface AppSettings {
|
|||||||
penalty_tiers: PenaltyTier[];
|
penalty_tiers: PenaltyTier[];
|
||||||
points_enabled: boolean;
|
points_enabled: boolean;
|
||||||
points_pools: PointsPool[];
|
points_pools: PointsPool[];
|
||||||
|
points_reward?: PointsReward | null;
|
||||||
referral_enabled: boolean;
|
referral_enabled: boolean;
|
||||||
delivery_schedule: DeliverySchedule;
|
delivery_schedule: DeliverySchedule;
|
||||||
postal_zones: PostalZone[];
|
postal_zones: PostalZone[];
|
||||||
@@ -1042,6 +1052,7 @@ export interface AppSettings {
|
|||||||
telegram_2fa_enabled: boolean;
|
telegram_2fa_enabled: boolean;
|
||||||
delivery_mode: DeliveryModeConfig;
|
delivery_mode: DeliveryModeConfig;
|
||||||
shop_name: string;
|
shop_name: string;
|
||||||
|
contact_telegram: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSettings = async (): Promise<{
|
export const getSettings = async (): Promise<{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import apiClient from "./client";
|
import apiClient from "./client";
|
||||||
import { API_BASE_URL } from "./client";
|
import { API_BASE_URL } from "./client";
|
||||||
|
|
||||||
|
//@ts
|
||||||
import type {
|
import type {
|
||||||
OrderItem,
|
OrderItem,
|
||||||
DeliveryPerson,
|
DeliveryPerson,
|
||||||
@@ -41,9 +42,6 @@ export const confirmReceptionCabine = async (commandId: number) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// PENALITES
|
|
||||||
|
|
||||||
export const applyClientPenalty = async (
|
export const applyClientPenalty = async (
|
||||||
clientUsername: string,
|
clientUsername: string,
|
||||||
reason: string,
|
reason: string,
|
||||||
@@ -71,7 +69,6 @@ export const resetClientPenalties = async (clientUsername: string) => {
|
|||||||
return { success: true, message: data.message };
|
return { success: true, message: data.message };
|
||||||
};
|
};
|
||||||
|
|
||||||
// pool: index dans pool_names/pool_keys, -1 = reset tous les points
|
|
||||||
export const resetClientPoints = async (
|
export const resetClientPoints = async (
|
||||||
clientUsername: string,
|
clientUsername: string,
|
||||||
pool: number = -1,
|
pool: number = -1,
|
||||||
@@ -97,10 +94,6 @@ export const getPenaltiesStats = async () => {
|
|||||||
return { success: true, data: data.data };
|
return { success: true, data: data.data };
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// COMMANDES
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
export const getCancelledOrders = async () => {
|
export const getCancelledOrders = async () => {
|
||||||
const { data } = await apiClient.get(`${API}/commands/cancelled`);
|
const { data } = await apiClient.get(`${API}/commands/cancelled`);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -42,10 +42,6 @@ export const updateMyStatus = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// QUEUE
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
export const getMyQueue = async (): Promise<{
|
export const getMyQueue = async (): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
queue_info?: QueueInfo;
|
queue_info?: QueueInfo;
|
||||||
@@ -62,10 +58,6 @@ export const getMyQueue = async (): Promise<{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// LIVRAISONS
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
export const getMyDeliveries = async (): Promise<{
|
export const getMyDeliveries = async (): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
deliveries?: DeliveryItem[];
|
deliveries?: DeliveryItem[];
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ const apiClient = axios.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// attach JWT token
|
|
||||||
apiClient.interceptors.request.use(async (config) => {
|
apiClient.interceptors.request.use(async (config) => {
|
||||||
const isAdminRoute =
|
const isAdminRoute =
|
||||||
config.url?.includes("/api/v2/") ||
|
config.url?.includes("/api/v2/") ||
|
||||||
@@ -27,7 +26,6 @@ apiClient.interceptors.request.use(async (config) => {
|
|||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Response interceptor: handle common errors
|
|
||||||
apiClient.interceptors.response.use(
|
apiClient.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
(error) => {
|
(error) => {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import axios from "axios";
|
|||||||
|
|
||||||
const TOMTOM_API_KEY = "MERY8I7LMeYVSLKO5WuV73W9rKJpBLoB";
|
const TOMTOM_API_KEY = "MERY8I7LMeYVSLKO5WuV73W9rKJpBLoB";
|
||||||
|
|
||||||
// ---- Types ----
|
|
||||||
export interface LatLng {
|
export interface LatLng {
|
||||||
latitude: number;
|
latitude: number;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
@@ -24,7 +23,6 @@ export interface NavigationInstruction {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Maneuver translations (FR) ----
|
|
||||||
export const maneuverTranslations: Record<string, string> = {
|
export const maneuverTranslations: Record<string, string> = {
|
||||||
TURN_LEFT: "Tournez à gauche",
|
TURN_LEFT: "Tournez à gauche",
|
||||||
TURN_RIGHT: "Tournez à droite",
|
TURN_RIGHT: "Tournez à droite",
|
||||||
@@ -48,7 +46,6 @@ export const maneuverTranslations: Record<string, string> = {
|
|||||||
WAYPOINT_REACHED: "Point de passage atteint",
|
WAYPOINT_REACHED: "Point de passage atteint",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ionicons name per maneuver
|
|
||||||
export const maneuverIcons: Record<string, string> = {
|
export const maneuverIcons: Record<string, string> = {
|
||||||
TURN_LEFT: "arrow-back",
|
TURN_LEFT: "arrow-back",
|
||||||
TURN_RIGHT: "arrow-forward",
|
TURN_RIGHT: "arrow-forward",
|
||||||
@@ -79,7 +76,6 @@ function formatDistance(meters: number): string {
|
|||||||
return `${(meters / 1000).toFixed(1)} km`;
|
return `${(meters / 1000).toFixed(1)} km`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Geocode address → coords ----
|
|
||||||
export async function geocodeAddress(address: string): Promise<LatLng | null> {
|
export async function geocodeAddress(address: string): Promise<LatLng | null> {
|
||||||
try {
|
try {
|
||||||
const url = `https://api.tomtom.com/search/2/geocode/${encodeURIComponent(address)}.json?key=${TOMTOM_API_KEY}&limit=1`;
|
const url = `https://api.tomtom.com/search/2/geocode/${encodeURIComponent(address)}.json?key=${TOMTOM_API_KEY}&limit=1`;
|
||||||
@@ -96,7 +92,6 @@ export async function geocodeAddress(address: string): Promise<LatLng | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Calculate route ----
|
|
||||||
export async function calculateRoute(
|
export async function calculateRoute(
|
||||||
origin: LatLng,
|
origin: LatLng,
|
||||||
destination: LatLng,
|
destination: LatLng,
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ import { useNavigation } from "@react-navigation/native";
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
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 { getSettings, updateSettings, getCategories, getAvailableDeliveryPersons, DEFAULT_DELIVERY_SCHEDULE, DEFAULT_POSTAL_ZONES } from "../../api/api_admin";
|
import { getSettings, updateSettings, getCategories, getAvailableDeliveryPersons, getAllProductsAdmin, DEFAULT_DELIVERY_SCHEDULE, DEFAULT_POSTAL_ZONES } from "../../api/api_admin";
|
||||||
import type { AppSettings, Category, CategoryRoute, DeliveryModeConfig, PointsTier, PointsPool, DaySchedule, DeliverySchedule, PostalZone } from "../../api/api_admin";
|
import type { AppSettings, Category, CategoryRoute, DeliveryModeConfig, PointsTier, PointsPool, PointsReward, RewardCategoryConfig, DaySchedule, DeliverySchedule, PostalZone } from "../../api/api_admin";
|
||||||
|
import type { Product } from "../../api/types";
|
||||||
import AlertModal from "../../components/ui/AlertModal";
|
import AlertModal from "../../components/ui/AlertModal";
|
||||||
import { useAlert } from "../../hooks/useAlert";
|
import { useAlert } from "../../hooks/useAlert";
|
||||||
|
|
||||||
@@ -643,6 +644,358 @@ function TiersSection({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const REWARD_ACCENT = "#f59e0b";
|
||||||
|
|
||||||
|
const REWARD_TYPES: { value: PointsReward["type"]; label: string; icon: string }[] = [
|
||||||
|
{ value: "free_product", label: "Produit offert", icon: "gift-outline" },
|
||||||
|
{ value: "half_price_product", label: "Produit à -50%", icon: "pricetag-outline" },
|
||||||
|
{ value: "custom", label: "Personnalisé", icon: "star-outline" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const EMPTY_REWARD: PointsReward = {
|
||||||
|
threshold: 20,
|
||||||
|
type: "free_product",
|
||||||
|
description: "",
|
||||||
|
category_configs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// Sélecteur de produits pour une catégorie dans la récompense
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
function CategoryProductPicker({
|
||||||
|
catConfig,
|
||||||
|
products,
|
||||||
|
onChange,
|
||||||
|
colors,
|
||||||
|
s,
|
||||||
|
}: {
|
||||||
|
catConfig: RewardCategoryConfig;
|
||||||
|
products: Product[];
|
||||||
|
onChange: (cfg: RewardCategoryConfig) => void;
|
||||||
|
colors: any;
|
||||||
|
s: any;
|
||||||
|
}) {
|
||||||
|
const catProducts = products.filter((p) => p.category === catConfig.category);
|
||||||
|
|
||||||
|
const toggleProduct = (id: number) => {
|
||||||
|
const ids = catConfig.product_ids.includes(id)
|
||||||
|
? catConfig.product_ids.filter((x) => x !== id)
|
||||||
|
: [...catConfig.product_ids, id];
|
||||||
|
onChange({ ...catConfig, product_ids: ids, all_products: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ marginTop: spacing.s, paddingLeft: spacing.m, gap: spacing.s }}>
|
||||||
|
{/* Montant pour cette catégorie */}
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.s }}>
|
||||||
|
<Text style={{ fontSize: 12, color: s.thresholdSep.color ?? "#888", flex: 1 }}>
|
||||||
|
Valeur du produit offert
|
||||||
|
</Text>
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
|
||||||
|
<TextInput
|
||||||
|
style={[s.thresholdInput, { width: 56, fontSize: 13 }]}
|
||||||
|
keyboardType="decimal-pad"
|
||||||
|
value={catConfig.amount > 0 ? String(catConfig.amount) : ""}
|
||||||
|
onChangeText={(v) => {
|
||||||
|
const n = parseFloat(v);
|
||||||
|
onChange({ ...catConfig, amount: isNaN(n) ? 0 : n });
|
||||||
|
}}
|
||||||
|
placeholder="0"
|
||||||
|
placeholderTextColor={colors.textMuted}
|
||||||
|
/>
|
||||||
|
<Text style={{ fontSize: 12, color: colors.textMuted }}>€</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Toggle tous / sélection */}
|
||||||
|
<View style={{ flexDirection: "row", gap: spacing.s }}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => onChange({ ...catConfig, all_products: true, product_ids: [] })}
|
||||||
|
style={{
|
||||||
|
flexDirection: "row", alignItems: "center", gap: spacing.xs,
|
||||||
|
paddingHorizontal: spacing.m, paddingVertical: spacing.xs,
|
||||||
|
borderRadius: borderRadius.full, borderWidth: 1.5,
|
||||||
|
borderColor: catConfig.all_products ? REWARD_ACCENT : colors.border,
|
||||||
|
backgroundColor: catConfig.all_products ? REWARD_ACCENT + "22" : "transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons name="checkmark-done-outline" size={13} color={catConfig.all_products ? REWARD_ACCENT : colors.textMuted} />
|
||||||
|
<Text style={{ fontSize: 12, fontWeight: catConfig.all_products ? "700" : "400", color: catConfig.all_products ? REWARD_ACCENT : colors.textMuted }}>
|
||||||
|
Tous ({catProducts.length})
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => onChange({ ...catConfig, all_products: false })}
|
||||||
|
style={{
|
||||||
|
flexDirection: "row", alignItems: "center", gap: spacing.xs,
|
||||||
|
paddingHorizontal: spacing.m, paddingVertical: spacing.xs,
|
||||||
|
borderRadius: borderRadius.full, borderWidth: 1.5,
|
||||||
|
borderColor: !catConfig.all_products ? REWARD_ACCENT : colors.border,
|
||||||
|
backgroundColor: !catConfig.all_products ? REWARD_ACCENT + "22" : "transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons name="list-outline" size={13} color={!catConfig.all_products ? REWARD_ACCENT : colors.textMuted} />
|
||||||
|
<Text style={{ fontSize: 12, fontWeight: !catConfig.all_products ? "700" : "400", color: !catConfig.all_products ? REWARD_ACCENT : colors.textMuted }}>
|
||||||
|
Sélection
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Liste des produits si mode sélection */}
|
||||||
|
{!catConfig.all_products && (
|
||||||
|
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: spacing.xs }}>
|
||||||
|
{catProducts.length === 0 ? (
|
||||||
|
<Text style={{ fontSize: 12, color: colors.textMuted, fontStyle: "italic" }}>
|
||||||
|
Aucun produit dans cette catégorie
|
||||||
|
</Text>
|
||||||
|
) : catProducts.map((p) => {
|
||||||
|
const sel = catConfig.product_ids.includes(p.id);
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={p.id}
|
||||||
|
onPress={() => toggleProduct(p.id)}
|
||||||
|
style={{
|
||||||
|
paddingHorizontal: spacing.s, paddingVertical: 4,
|
||||||
|
borderRadius: borderRadius.sm, borderWidth: 1.5,
|
||||||
|
borderColor: sel ? REWARD_ACCENT : colors.border,
|
||||||
|
backgroundColor: sel ? REWARD_ACCENT + "22" : "transparent",
|
||||||
|
flexDirection: "row", alignItems: "center", gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sel && <Ionicons name="checkmark" size={11} color={REWARD_ACCENT} />}
|
||||||
|
<Text style={{ fontSize: 12, fontWeight: sel ? "700" : "400", color: sel ? REWARD_ACCENT : colors.textMuted }}>
|
||||||
|
{p.name}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// Section centralisée récompenses par palier
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
function CentralRewardSection({
|
||||||
|
reward,
|
||||||
|
allCategories,
|
||||||
|
productsByCategory,
|
||||||
|
onChange,
|
||||||
|
colors,
|
||||||
|
s,
|
||||||
|
}: {
|
||||||
|
reward: PointsReward | null | undefined;
|
||||||
|
allCategories: Category[];
|
||||||
|
productsByCategory: Record<string, Product[]>;
|
||||||
|
onChange: (reward: PointsReward | null) => void;
|
||||||
|
colors: any;
|
||||||
|
s: any;
|
||||||
|
}) {
|
||||||
|
const { width: screenWidth } = useWindowDimensions();
|
||||||
|
const enabled = !!reward;
|
||||||
|
const r = reward ?? EMPTY_REWARD;
|
||||||
|
|
||||||
|
const update = (patch: Partial<PointsReward>) =>
|
||||||
|
onChange({ ...r, ...patch });
|
||||||
|
|
||||||
|
const getCatConfig = (catName: string): RewardCategoryConfig =>
|
||||||
|
r.category_configs.find((c) => c.category === catName) ??
|
||||||
|
{ category: catName, all_products: true, product_ids: [], amount: 0 };
|
||||||
|
|
||||||
|
const isCatSelected = (catName: string) =>
|
||||||
|
r.category_configs.some((c) => c.category === catName);
|
||||||
|
|
||||||
|
const toggleCategory = (catName: string) => {
|
||||||
|
if (isCatSelected(catName)) {
|
||||||
|
update({ category_configs: r.category_configs.filter((c) => c.category !== catName) });
|
||||||
|
} else {
|
||||||
|
update({ category_configs: [...r.category_configs, { category: catName, all_products: true, product_ids: [], amount: 0 }] });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCatConfig = (cfg: RewardCategoryConfig) => {
|
||||||
|
update({
|
||||||
|
category_configs: r.category_configs.map((c) =>
|
||||||
|
c.category === cfg.category ? cfg : c
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={s.section}>
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingRight: spacing.l }}>
|
||||||
|
<Text style={s.sectionTitle}>Récompenses par palier</Text>
|
||||||
|
<View style={{
|
||||||
|
paddingHorizontal: spacing.s, paddingVertical: 2, borderRadius: 10,
|
||||||
|
backgroundColor: enabled ? REWARD_ACCENT + "25" : colors.border + "40",
|
||||||
|
borderWidth: 1, borderColor: enabled ? REWARD_ACCENT : colors.border,
|
||||||
|
marginBottom: spacing.s,
|
||||||
|
}}>
|
||||||
|
<Text style={{ fontSize: 10, fontWeight: "700", color: enabled ? REWARD_ACCENT : colors.textMuted }}>
|
||||||
|
{enabled ? "Activée" : "Désactivée"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[s.row, s.rowFirst]}>
|
||||||
|
<View style={s.rowLeft}>
|
||||||
|
<Text style={s.rowLabel}>Récompense activée</Text>
|
||||||
|
<Text style={s.rowDesc}>
|
||||||
|
Le seuil s'applique indépendamment à chaque type de points.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={enabled}
|
||||||
|
onValueChange={(v) => onChange(v ? EMPTY_REWARD : null)}
|
||||||
|
trackColor={{ false: colors.border, true: REWARD_ACCENT }}
|
||||||
|
thumbColor="#fff"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{enabled && (
|
||||||
|
<View style={{ borderTopWidth: 1, borderTopColor: colors.border, paddingHorizontal: spacing.l, paddingTop: spacing.l, paddingBottom: spacing.l, gap: spacing.l }}>
|
||||||
|
|
||||||
|
{/* Seuil */}
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.m }}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={s.rowLabel}>Seuil de points</Text>
|
||||||
|
<Text style={s.rowDesc}>
|
||||||
|
Dès X points cumulés dans un type, la récompense est débloquée pour ce type.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
||||||
|
<TextInput
|
||||||
|
style={[s.thresholdInput, { width: screenWidth < 380 ? 52 : 64 }]}
|
||||||
|
keyboardType="number-pad"
|
||||||
|
value={String(r.threshold)}
|
||||||
|
onChangeText={(v) => { const n = parseInt(v, 10); if (!isNaN(n) && n > 0) update({ threshold: n }); }}
|
||||||
|
/>
|
||||||
|
<Text style={s.thresholdSep}>pts</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Type */}
|
||||||
|
<View>
|
||||||
|
<Text style={[s.rowLabel, { marginBottom: spacing.s }]}>Type de récompense</Text>
|
||||||
|
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: spacing.s }}>
|
||||||
|
{REWARD_TYPES.map((rt) => {
|
||||||
|
const sel = r.type === rt.value;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={rt.value}
|
||||||
|
onPress={() => update({ type: rt.value })}
|
||||||
|
style={{
|
||||||
|
flexDirection: "row", alignItems: "center", gap: spacing.xs,
|
||||||
|
paddingHorizontal: spacing.m, paddingVertical: spacing.s,
|
||||||
|
borderRadius: borderRadius.sm, borderWidth: 1.5,
|
||||||
|
borderColor: sel ? REWARD_ACCENT : colors.border,
|
||||||
|
backgroundColor: sel ? REWARD_ACCENT + "20" : "transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons name={rt.icon as any} size={14} color={sel ? REWARD_ACCENT : colors.textMuted} />
|
||||||
|
<Text style={{ fontSize: 13, fontWeight: sel ? "700" : "400", color: sel ? REWARD_ACCENT : colors.textMuted }}>
|
||||||
|
{rt.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<View>
|
||||||
|
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Description affichée au client</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[s.thresholdInput, { width: "100%", textAlign: "left", paddingHorizontal: spacing.m, paddingVertical: spacing.s, height: 72, textAlignVertical: "top" }]}
|
||||||
|
value={r.description}
|
||||||
|
onChangeText={(v) => update({ description: v })}
|
||||||
|
placeholder="Ex : Un produit 30€ de ton choix parmi les produits conditionnés en 30€"
|
||||||
|
placeholderTextColor={colors.textMuted}
|
||||||
|
multiline
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Catégories éligibles */}
|
||||||
|
<View>
|
||||||
|
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Catégories éligibles</Text>
|
||||||
|
<Text style={[s.rowDesc, { marginBottom: spacing.m }]}>
|
||||||
|
Sélectionnez les catégories, puis pour chacune choisissez tous les produits ou une sélection.
|
||||||
|
</Text>
|
||||||
|
{allCategories.length === 0 ? (
|
||||||
|
<Text style={[s.hint, { paddingHorizontal: 0 }]}>Aucune catégorie disponible</Text>
|
||||||
|
) : (
|
||||||
|
<View style={{ gap: spacing.m }}>
|
||||||
|
{allCategories.map((cat) => {
|
||||||
|
const selected = isCatSelected(cat.name);
|
||||||
|
const catColor = cat.color || REWARD_ACCENT;
|
||||||
|
return (
|
||||||
|
<View key={cat.name}>
|
||||||
|
{/* Chip catégorie */}
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => toggleCategory(cat.name)}
|
||||||
|
style={{
|
||||||
|
flexDirection: "row", alignItems: "center", gap: spacing.xs,
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
paddingHorizontal: spacing.m, paddingVertical: spacing.s,
|
||||||
|
borderRadius: borderRadius.full, borderWidth: 1.5,
|
||||||
|
borderColor: selected ? catColor : colors.border,
|
||||||
|
backgroundColor: selected ? catColor + "22" : "transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: catColor }} />
|
||||||
|
<Text style={{ fontSize: 13, fontWeight: selected ? "700" : "400", color: selected ? catColor : colors.textMuted }}>
|
||||||
|
{cat.name}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name={selected ? "chevron-down" : "chevron-forward"}
|
||||||
|
size={12}
|
||||||
|
color={selected ? catColor : colors.textMuted}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Sélecteur produits (visible si catégorie sélectionnée) */}
|
||||||
|
{selected && (
|
||||||
|
<CategoryProductPicker
|
||||||
|
catConfig={getCatConfig(cat.name)}
|
||||||
|
products={productsByCategory[cat.name] ?? []}
|
||||||
|
onChange={updateCatConfig}
|
||||||
|
colors={colors}
|
||||||
|
s={s}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Récapitulatif */}
|
||||||
|
{r.category_configs.length > 0 && (
|
||||||
|
<View style={{ backgroundColor: REWARD_ACCENT + "12", borderRadius: borderRadius.sm, borderLeftWidth: 3, borderLeftColor: REWARD_ACCENT, padding: spacing.m, gap: 4 }}>
|
||||||
|
<Text style={{ fontSize: 13, fontWeight: "700", color: REWARD_ACCENT }}>Récapitulatif</Text>
|
||||||
|
<Text style={{ fontSize: 13, color: colors.textPrimary }}>
|
||||||
|
Dès <Text style={{ fontWeight: "700" }}>{r.threshold} pts</Text> par type →{" "}
|
||||||
|
{REWARD_TYPES.find((x) => x.value === r.type)?.label}
|
||||||
|
</Text>
|
||||||
|
{r.description !== "" && (
|
||||||
|
<Text style={{ fontSize: 12, color: colors.textMuted, fontStyle: "italic" }}>"{r.description}"</Text>
|
||||||
|
)}
|
||||||
|
{r.category_configs.map((cfg) => (
|
||||||
|
<Text key={cfg.category} style={{ fontSize: 12, color: colors.textSecondary }}>
|
||||||
|
• {cfg.category}{cfg.amount > 0 ? ` (${cfg.amount}€)` : ""} : {cfg.all_products ? "tous les produits" : `${cfg.product_ids.length} produit(s) sélectionné(s)`}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function SettingsScreen() {
|
export default function SettingsScreen() {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { alert, showError, showSuccess, hideAlert } = useAlert();
|
const { alert, showError, showSuccess, hideAlert } = useAlert();
|
||||||
@@ -680,10 +1033,13 @@ export default function SettingsScreen() {
|
|||||||
telegram_2fa_enabled: false,
|
telegram_2fa_enabled: false,
|
||||||
delivery_mode: { mode: "single" as const, category_routes: [] },
|
delivery_mode: { mode: "single" as const, category_routes: [] },
|
||||||
shop_name: "Milieu-Nantais",
|
shop_name: "Milieu-Nantais",
|
||||||
|
contact_telegram: "",
|
||||||
|
points_reward: null,
|
||||||
});
|
});
|
||||||
const [showApiKey, setShowApiKey] = useState(false);
|
const [showApiKey, setShowApiKey] = useState(false);
|
||||||
const [showIpnSecret, setShowIpnSecret] = useState(false);
|
const [showIpnSecret, setShowIpnSecret] = useState(false);
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
|
const [productsByCategory, setProductsByCategory] = useState<Record<string, Product[]>>({});
|
||||||
const [livreurs, setLivreurs] = useState<string[]>([]);
|
const [livreurs, setLivreurs] = useState<string[]>([]);
|
||||||
|
|
||||||
// Refs pour l'auto-sauvegarde au départ de la page
|
// Refs pour l'auto-sauvegarde au départ de la page
|
||||||
@@ -713,10 +1069,11 @@ export default function SettingsScreen() {
|
|||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
isLoaded.current = false;
|
isLoaded.current = false;
|
||||||
const [settingsRes, categoriesRes, livreursRes] = await Promise.all([
|
const [settingsRes, categoriesRes, livreursRes, productsRes] = await Promise.all([
|
||||||
getSettings(),
|
getSettings(),
|
||||||
getCategories(),
|
getCategories(),
|
||||||
getAvailableDeliveryPersons(),
|
getAvailableDeliveryPersons(),
|
||||||
|
getAllProductsAdmin(),
|
||||||
]);
|
]);
|
||||||
if (settingsRes.success && settingsRes.settings) {
|
if (settingsRes.success && settingsRes.settings) {
|
||||||
setSettings({
|
setSettings({
|
||||||
@@ -725,6 +1082,7 @@ export default function SettingsScreen() {
|
|||||||
delivery_schedule: settingsRes.settings.delivery_schedule ?? DEFAULT_DELIVERY_SCHEDULE,
|
delivery_schedule: settingsRes.settings.delivery_schedule ?? DEFAULT_DELIVERY_SCHEDULE,
|
||||||
postal_zones: settingsRes.settings.postal_zones ?? DEFAULT_POSTAL_ZONES,
|
postal_zones: settingsRes.settings.postal_zones ?? DEFAULT_POSTAL_ZONES,
|
||||||
delivery_mode: settingsRes.settings.delivery_mode ?? { mode: "single", category_routes: [] },
|
delivery_mode: settingsRes.settings.delivery_mode ?? { mode: "single", category_routes: [] },
|
||||||
|
points_reward: settingsRes.settings.points_reward ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (categoriesRes) {
|
if (categoriesRes) {
|
||||||
@@ -733,6 +1091,15 @@ export default function SettingsScreen() {
|
|||||||
if (livreursRes.success) {
|
if (livreursRes.success) {
|
||||||
setLivreurs(livreursRes.livreurs.map((l: any) => l.username));
|
setLivreurs(livreursRes.livreurs.map((l: any) => l.username));
|
||||||
}
|
}
|
||||||
|
if (productsRes.success) {
|
||||||
|
const byCategory: Record<string, Product[]> = {};
|
||||||
|
for (const p of productsRes.data) {
|
||||||
|
const cat = p.category || "";
|
||||||
|
if (!byCategory[cat]) byCategory[cat] = [];
|
||||||
|
byCategory[cat].push(p);
|
||||||
|
}
|
||||||
|
setProductsByCategory(byCategory);
|
||||||
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
// Marquer comme chargé après un tick pour que le useEffect de settings ne
|
// Marquer comme chargé après un tick pour que le useEffect de settings ne
|
||||||
// considère pas le setSettings initial comme une modification utilisateur
|
// considère pas le setSettings initial comme une modification utilisateur
|
||||||
@@ -1256,6 +1623,16 @@ export default function SettingsScreen() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Récompense centralisée par palier */}
|
||||||
|
<CentralRewardSection
|
||||||
|
reward={settings.points_reward ?? null}
|
||||||
|
allCategories={categories}
|
||||||
|
productsByCategory={productsByCategory}
|
||||||
|
onChange={(reward) => setSettings((p) => ({ ...p, points_reward: reward }))}
|
||||||
|
colors={colors}
|
||||||
|
s={s}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Horaires de livraison */}
|
{/* Horaires de livraison */}
|
||||||
<DeliveryScheduleSection
|
<DeliveryScheduleSection
|
||||||
schedule={settings.delivery_schedule ?? DEFAULT_DELIVERY_SCHEDULE}
|
schedule={settings.delivery_schedule ?? DEFAULT_DELIVERY_SCHEDULE}
|
||||||
@@ -1486,6 +1863,30 @@ export default function SettingsScreen() {
|
|||||||
<Text style={{ color: "#10b981", fontSize: fontSize.sm }}>Bot configuré — @{settings.telegram_bot_username}</Text>
|
<Text style={{ color: "#10b981", fontSize: fontSize.sm }}>Bot configuré — @{settings.telegram_bot_username}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
<View>
|
||||||
|
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Contact SAV Telegram</Text>
|
||||||
|
<Text style={[s.rowDesc, { marginBottom: spacing.s }]}>
|
||||||
|
Username du compte Telegram SAV (sans le @). Utilisé pour les boutons de contact client.
|
||||||
|
</Text>
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.s }}>
|
||||||
|
<Text style={{ fontSize: fontSize.md, color: colors.textMuted }}>@</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[s.input, { flex: 1 }]}
|
||||||
|
value={settings.contact_telegram}
|
||||||
|
onChangeText={(v) => setSettings((p) => ({ ...p, contact_telegram: v }))}
|
||||||
|
placeholder="MonSAV"
|
||||||
|
placeholderTextColor={colors.textMuted}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
{settings.contact_telegram !== "" && (
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.s, marginTop: spacing.s, padding: spacing.m, backgroundColor: "#0088cc22", borderRadius: borderRadius.sm }}>
|
||||||
|
<Ionicons name="paper-plane-outline" size={16} color="#0088cc" />
|
||||||
|
<Text style={{ color: "#0088cc", fontSize: fontSize.sm }}>@{settings.contact_telegram}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user