Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 76 additions & 1 deletion .bitrise/bitrise.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ trigger_map:
workflow: android-only
- push_branch: master
workflow: app_distribution_to_uat_testers
# Build a preview (APK + Appetize deploy) for every pull request into develop.
- pull_request_target_branch: develop
enabled: false
workflow: android-only
- pull_request_source_branch: "*"
enabled: false
Expand Down Expand Up @@ -133,10 +133,54 @@ step_bundles:
- notify_email_list: "$TESTERS"
- notify_user_groups: owner, admins, developers
is_always_run: false
# Pick which Appetize app to deploy to based on build type: PR builds go to
# the PR preview app, everything else (e.g. develop push) to the main app.
# BITRISE_PULL_REQUEST is set (to the PR number) only on pull request builds.
- script@1:
title: Select Appetize app by build type
inputs:
- content: |-
set -euo pipefail
if [ -n "${BITRISE_PULL_REQUEST:-}" ]; then
echo "PR build - deploying to PR preview app"
envman add --key APPETIZE_PUBLIC_KEY --value "${APPETIZE_PR_PUBLIC_KEY:-}"
else
echo "Push build - deploying to main app"
envman add --key APPETIZE_PUBLIC_KEY --value "${APPETIZE_DEVELOP_PUBLIC_KEY:-}"
fi
- appetize-deploy@0:
# Skip the deploy when no target app key is configured, so we never fall
# back to creating a new app (which would overwrite the main app).
run_if: '{{enveq "APPETIZE_PUBLIC_KEY" "" | not}}'
inputs:
- app_path: $BITRISE_DEPLOY_DIR/openboxes_test_b$BITRISE_BUILD_NUMBER.apk
- appetize_token: $APPETIZE_API_TOKEN
- public_key: $APPETIZE_PUBLIC_KEY
# Annotate the deployed Appetize app with build context (PR/branch/commit).
# The appetize-deploy step can't set a note, so call the v1 API directly.
# v1 supports "note" but not "tags"; failures here are non-fatal.
- script@1:
title: Set Appetize note with build context
run_if: '{{enveq "APPETIZE_PUBLIC_KEY" "" | not}}'
inputs:
- content: |-
set -euo pipefail
SHA="${BITRISE_GIT_COMMIT:-}"
SHA="${SHA:0:8}"
if [ -n "${BITRISE_PULL_REQUEST:-}" ]; then
NOTE="PR #${BITRISE_PULL_REQUEST} - ${BITRISE_GIT_BRANCH:-} - ${SHA} - build ${BITRISE_BUILD_NUMBER:-}"
else
NOTE="${BITRISE_GIT_BRANCH:-} - ${SHA} - build ${BITRISE_BUILD_NUMBER:-}"
fi
BODY="$(jq -n --arg note "$NOTE" '{note: $note}')"
if curl -fsS -X POST "https://api.appetize.io/v1/apps/${APPETIZE_PUBLIC_KEY}" \
-H "X-API-KEY: ${APPETIZE_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "$BODY" > /dev/null; then
echo "Set Appetize note: ${NOTE}"
else
echo "Warning: failed to set Appetize note (non-fatal)"
fi
release-steps-android-signed:
description: |-
Assemble the Android app and copy the signed APK to $BITRISE_DEPLOY_DIR.
Expand Down Expand Up @@ -229,6 +273,26 @@ workflows:
- bundle::setup-steps: {}
- bundle::setup-steps-android: {}
- bundle::branding: {}
# On PR builds only, mark the build as experimental: append "(Experimental)"
# to the app name AND give it a distinct applicationId. Appetize keys apps by
# applicationId, so the suffix makes the PR build a separate Appetize app
# (its own URL) instead of overwriting the stable app. Runs after branding
# (which sets app_name) and before the release assemble.
- script@1:
title: Mark PR builds as experimental (name + applicationId)
run_if: '{{getenv "BITRISE_PULL_REQUEST" | ne ""}}'
inputs:
- content: |-
set -euo pipefail
STRINGS_FILE="android/app/src/main/res/values/strings.xml"
GRADLE_FILE="android/app/build.gradle"
sed -i.bak -E 's#(<string name="app_name">)([^<]*)(</string>)#\1\2 (Experimental)\3#' "$STRINGS_FILE"
rm -f "$STRINGS_FILE.bak"
sed -i.bak -E 's#(applicationId "com\.openboxes\.android)(")#\1.experimental\2#' "$GRADLE_FILE"
rm -f "$GRADLE_FILE.bak"
echo "app_name and applicationId are now:"
grep app_name "$STRINGS_FILE"
grep applicationId "$GRADLE_FILE"
- bundle::release-steps-android: {}
triggers:
enabled: false
Expand Down Expand Up @@ -283,6 +347,17 @@ app:
- opts:
is_expand: false
VARIANT: staging
# Appetize app public keys (from the app's share URL: appetize.io/app/<key>).
# These are not secrets. The main app is used by develop; PR builds deploy to a
# separate PR preview app. Leave APPETIZE_PR_PUBLIC_KEY empty until the PR
# preview app exists — the appetize-deploy run_if guard skips the deploy while
# it is empty, so PR builds never overwrite the main app.
- opts:
is_expand: false
APPETIZE_DEVELOP_PUBLIC_KEY: 67t5r4akxrtgvujo7adbiifau4
- opts:
is_expand: false
APPETIZE_PR_PUBLIC_KEY: "b_wchsvah7t6ytekpcvd2pvqavty"

meta:
bitrise.io:
Expand Down
6 changes: 6 additions & 0 deletions src/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import CycleCountListEntry from './screens/CycleCount/CycleCountListEntry';
import CycleCountLocation from './screens/CycleCount/CycleCountLocation';
import CycleCountProduct from './screens/CycleCount/CycleCountProduct';
import CycleCountQuantityAvailable from './screens/CycleCount/CycleCountQuantityAvailable';
import DiscretePickingListScreen from './screens/Picking/DiscretePickingListScreen';
import PickingPickTypeScreen from './screens/Picking/PickingPickTypeScreen';
import PickingPickLocationScreen from './screens/Picking/PickingPickLocationScreen';
import PickingPickProductScreen from './screens/Picking/PickingPickProductScreen';
Expand Down Expand Up @@ -338,6 +339,11 @@ class Main extends Component<Props, State> {
component={PutawayQuantityScreen}
options={{ title: 'Putaway Details' }}
/>
<Stack.Screen
name="DiscretePickingList"
component={DiscretePickingListScreen}
options={{ title: 'Discrete Picking' }}
/>
<Stack.Screen
name="PickingPickType"
component={PickingPickTypeScreen}
Expand Down
25 changes: 20 additions & 5 deletions src/apis/picking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,28 @@ export type PickTaskDropParams = {
stagedById: string;
};

export function getPickTasksApi(facilityId: string, params: PickTaskParams) {
const { deliveryTypeCode, ordersCount } = params;
export function getPickTasksApi(facilityId: string, params?: Partial<PickTaskParams>) {
const query: string[] = [];

if (params?.deliveryTypeCode) {
query.push(`deliveryTypeCode=${encodeURIComponent(params.deliveryTypeCode)}`);
}

if (params?.ordersCount !== undefined && params?.ordersCount !== null) {
query.push(`ordersCount=${encodeURIComponent(params.ordersCount)}`);
}

const queryString = query.length > 0 ? `?${query.join('&')}` : '';

return ApiClient.get(`/facilities/${facilityId}/pick-tasks${queryString}`);
}

// Fetch all open pick tasks (across every queue type) for the discrete picking order list.
export function getOpenPickTasksApi(facilityId: string) {
const statuses = ['PENDING', 'PICKING'];

return ApiClient.get(
`/facilities/${facilityId}/pick-tasks?deliveryTypeCode=${encodeURIComponent(
deliveryTypeCode
)}&ordersCount=${encodeURIComponent(ordersCount)}`
`/facilities/${facilityId}/pick-tasks?${statuses.map((status) => `status=${encodeURIComponent(status)}`).join('&')}`
);
}

Expand Down
23 changes: 23 additions & 0 deletions src/redux/actions/picking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export const GET_PICK_TASKS_REQUEST = 'GET_PICK_TASKS_REQUEST';
export const GET_PICK_TASKS_REQUEST_SUCCESS = 'GET_PICK_TASKS_REQUEST_SUCCESS';
export const GET_PICK_TASKS_REQUEST_FAIL = 'GET_PICK_TASKS_REQUEST_FAIL';

export const GET_OPEN_PICK_TASKS_REQUEST = 'GET_OPEN_PICK_TASKS_REQUEST';
export const GET_OPEN_PICK_TASKS_REQUEST_SUCCESS = 'GET_OPEN_PICK_TASKS_REQUEST_SUCCESS';
export const GET_OPEN_PICK_TASKS_REQUEST_FAIL = 'GET_OPEN_PICK_TASKS_REQUEST_FAIL';

export const GET_PICK_TASK_COUNTS_REQUEST = 'GET_PICK_TASK_COUNTS_REQUEST';
export const GET_PICK_TASK_COUNTS_REQUEST_SUCCESS = 'GET_PICK_TASK_COUNTS_REQUEST_SUCCESS';
export const GET_PICK_TASK_COUNTS_REQUEST_FAIL = 'GET_PICK_TASK_COUNTS_REQUEST_FAIL';
Expand Down Expand Up @@ -66,6 +70,25 @@ export function getPickTasksAction(
};
}

export function getOpenPickTasksAction(
callback: (response: {
response?: {
data: PickTask[];
errorCode?: string;
message?: string;
max?: number;
offset?: number;
totalCount?: number;
};
errorMessage?: string;
}) => void
) {
return {
type: GET_OPEN_PICK_TASKS_REQUEST,
callback
};
}

export function getPickTaskCountsAction(
callback: (response: { response?: { data: DeliveryTypeOrderCount[] }; errorMessage?: string }) => void
) {
Expand Down
26 changes: 26 additions & 0 deletions src/redux/sagas/picking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import {
GET_PICK_TASKS_BY_REQUISITION_REQUEST,
GET_PICK_TASKS_BY_REQUISITION_REQUEST_SUCCESS,
GET_PICK_TASKS_BY_REQUISITION_REQUEST_FAIL,
GET_OPEN_PICK_TASKS_REQUEST,
GET_OPEN_PICK_TASKS_REQUEST_FAIL,
GET_OPEN_PICK_TASKS_REQUEST_SUCCESS,
GET_PICK_TASK_COUNTS_REQUEST,
GET_PICK_TASK_COUNTS_REQUEST_FAIL,
GET_PICK_TASK_COUNTS_REQUEST_SUCCESS,
Expand Down Expand Up @@ -61,6 +64,28 @@ function* getPickTasksAction(action: any) {
}
}

function* getOpenPickTasksAction(action: any) {
try {
// @ts-ignore
const currentLocation = yield select(userLocation);
if (!currentLocation) {
throw new Error('User Location Not Found');
}
yield put(showScreenLoading('Fetching Orders...'));
// Call API to get all open pick tasks across every queue type
// @ts-ignore
const response = yield call(api.getOpenPickTasksApi, currentLocation.id);
yield action.callback({ response });
yield put({ type: GET_OPEN_PICK_TASKS_REQUEST_SUCCESS, payload: response.data });
yield put(hideScreenLoading());
} catch (error) {
const errorMessage = (error as any)?.message || 'Error Fetching Orders';
yield put({ type: GET_OPEN_PICK_TASKS_REQUEST_FAIL, payload: errorMessage });
yield action.callback({ errorMessage });
yield put(hideScreenLoading());
}
}

function* getPickTaskCountsAction(action: any) {
try {
// @ts-ignore
Expand Down Expand Up @@ -346,6 +371,7 @@ function* reallocatePickTaskAction(action: any) {

export default function* watcher() {
yield takeLatest(GET_PICK_TASKS_REQUEST, getPickTasksAction);
yield takeLatest(GET_OPEN_PICK_TASKS_REQUEST, getOpenPickTasksAction);
yield takeLatest(GET_PICK_TASK_COUNTS_REQUEST, getPickTaskCountsAction);
yield takeLatest(START_PICK_TASK_REQUEST, startPickTaskAction);
yield takeLatest(PICK_PICK_TASK_REQUEST, pickPickTaskAction);
Expand Down
8 changes: 8 additions & 0 deletions src/screens/Dashboard/dashboardData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ const dashboardEntries: DashboardEntry[] = [
navigationScreenName: 'PickingPickType',
group: 'OUTBOUND'
},
{
key: 'discretePicking',
screenName: 'Discrete Picking',
entryDescription: 'Find and pick a single open order',
icon: IconPicking,
navigationScreenName: 'DiscretePickingList',
group: 'OUTBOUND'
},
{
key: 'packing',
screenName: 'Packing',
Expand Down
51 changes: 51 additions & 0 deletions src/screens/Picking/DiscretePickingCardSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Card } from 'react-native-paper';

import { ShimmerBlock, SkeletonDivider } from '../../components/ContentSkeleton';
import Theme from '../../utils/Theme';

export default function DiscretePickingCardSkeleton() {
return (
<View style={styles.wrapper}>
<Card style={styles.card}>
<Card.Content>
<View style={styles.headerRow}>
<ShimmerBlock style={styles.orderNumber} />
<ShimmerBlock style={styles.statusChip} />
</View>
<SkeletonDivider />
<ShimmerBlock style={styles.destination} />
<View style={styles.metaRow}>
<ShimmerBlock style={styles.metaChip} />
<ShimmerBlock style={styles.metaChip} />
</View>
</Card.Content>
</Card>
</View>
);
}

const styles = StyleSheet.create({
wrapper: {
marginHorizontal: Theme.spacing.medium,
marginBottom: Theme.spacing.small - 2
},
card: {
borderRadius: Theme.roundness,
backgroundColor: 'white',
borderWidth: 1,
borderColor: '#ddd'
},
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: Theme.spacing.small - 2
},
orderNumber: { width: 140, height: 22, borderRadius: 4 },
statusChip: { width: 90, height: 24, borderRadius: Theme.roundness },
destination: { width: '70%', height: 16, borderRadius: 4, marginTop: 6 },
metaRow: { flexDirection: 'row', marginTop: Theme.spacing.small },
metaChip: { width: 90, height: 24, borderRadius: Theme.roundness, marginRight: Theme.spacing.small }
});
Loading