products = helper.getProducts(activeUserId);
StringBuffer sb = new StringBuffer();
- for (Post p : posts) {
- sb.append(p.text + "\n");
+ for (Product p : products) {
+ sb.append(p.name + "\n");
}
TextView tv = (TextView) getView().findViewById(R.id.text_home);
@@ -117,16 +122,59 @@ public void onDetach() {
mListener = null;
}
- /**
- * This interface must be implemented by activities that contain this
- * fragment to allow an interaction in this fragment to be communicated
- * to the activity and potentially other fragments contained in that
- * activity.
- *
- * See the Android Training lesson Communicating with Other Fragments for more information.
- */
+ void displayData(int userId) {
+ final BukaAnalyticsSqliteOpenHelper db = BukaAnalyticsSqliteOpenHelper.getInstance(getContext());
+
+ List products = db.getProducts(userId);
+
+ //Construct the string
+ StringBuffer sb = new StringBuffer();
+
+ if(products.size() > 3) {
+ List stats0 = db.getStats(products.get(0).id);
+ List stats1 = db.getStats(products.get(1).id);
+ List stats2 = db.getStats(products.get(2).id);
+
+ if(stats0 != null && stats0.size() > 0) {
+ sb.append(products.get(0).id + "\n");
+ for (Stat s : stats0) {
+ sb.append(String.format("%s %d %d %d %d %d %d \n", s.date, s.viewCount, s.totalViewCount, s.soldCount, s.totalSoldCount, s.interestCount, s.totalInterestCount));
+ }
+ sb.append("\n");
+ }
+
+ if(stats1 != null && stats1.size() > 0) {
+ sb.append(products.get(1).id + "\n");
+ for (Stat s : stats1) {
+ sb.append(String.format("%s %d %d %d %d %d %d \n", s.date, s.viewCount, s.totalViewCount, s.soldCount, s.totalSoldCount, s.interestCount, s.totalInterestCount));
+ }
+ sb.append("\n");
+ }
+
+ if(stats2 != null && stats2.size() > 0) {
+ sb.append(products.get(2).id + "\n");
+ for (Stat s : stats2) {
+ sb.append(String.format("%s %d %d %d %d %d %d \n", s.date, s.viewCount, s.totalViewCount, s.soldCount, s.totalSoldCount, s.interestCount, s.totalInterestCount));
+ }
+ sb.append("\n");
+ }
+ }
+
+ TextView tv = (TextView) getView().findViewById(R.id.text_home);
+ tv.setText(sb.toString());
+ tv.setMovementMethod(new ScrollingMovementMethod());
+ }
+
+ /**
+ * This interface must be implemented by activities that contain this
+ * fragment to allow an interaction in this fragment to be communicated
+ * to the activity and potentially other fragments contained in that
+ * activity.
+ *
+ * See the Android Training lesson Communicating with Other Fragments for more information.
+ */
public interface OnFragmentInteractionListener {
// TODO: Update argument type and name
void onFragmentInteraction(Uri uri);
diff --git a/app/src/main/java/com/github/bukaanalytics/widget/BukaAnalyticsAppWidgetProvider.java b/app/src/main/java/com/github/bukaanalytics/widget/BukaAnalyticsAppWidgetProvider.java
index 9a0ba69..fe6de8d 100644
--- a/app/src/main/java/com/github/bukaanalytics/widget/BukaAnalyticsAppWidgetProvider.java
+++ b/app/src/main/java/com/github/bukaanalytics/widget/BukaAnalyticsAppWidgetProvider.java
@@ -3,12 +3,23 @@
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.view.View;
import android.widget.RemoteViews;
import com.github.bukaanalytics.R;
+import com.github.bukaanalytics.common.HTTPRequestHelper;
+import com.github.bukaanalytics.common.model.BukaAnalyticsSqliteOpenHelper;
+import com.github.bukaanalytics.common.model.Token;
+import com.github.bukaanalytics.widget.widgetalarm.WidgetAlarm;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
import java.util.Random;
/**
@@ -16,16 +27,30 @@
*/
public class BukaAnalyticsAppWidgetProvider extends AppWidgetProvider {
+
+ public static final String ACTION_AUTO_UPDATE = "AUTO_UPDATE";
+
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ final BukaAnalyticsSqliteOpenHelper db = BukaAnalyticsSqliteOpenHelper.getInstance(context.getApplicationContext());
final int count = appWidgetIds.length;
for (int i = 0; i < count; i++) {
int widgetId = appWidgetIds[i];
- String number = String.format("%03d", (new Random().nextInt(900) + 100));
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
- remoteViews.setTextViewText(R.id.textView_content_unread_msg, number);
+
+ Token tokenData = db.getTokenData();
+ if (tokenData.id == 0) {
+ remoteViews.setViewVisibility(R.id.textView_not_login, View.VISIBLE);
+ remoteViews.setViewVisibility(R.id.layout_stat, View.GONE);
+ } else {
+ remoteViews.setViewVisibility(R.id.textView_not_login, View.GONE);
+ remoteViews.setViewVisibility(R.id.layout_stat, View.VISIBLE);
+ getUnread(context, appWidgetManager, remoteViews, widgetId);
+ getNotifByType("report", context, appWidgetManager, remoteViews, widgetId);
+ getNotifByType("nego", context, appWidgetManager, remoteViews, widgetId);
+ }
Intent intent = new Intent(context, BukaAnalyticsAppWidgetProvider.class);
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
@@ -33,8 +58,66 @@ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] a
PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ remoteViews.setOnClickPendingIntent(R.id.layout_container, pendingIntent);
appWidgetManager.updateAppWidget(widgetId, remoteViews);
}
}
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ super.onReceive(context, intent);
+ if(intent.getAction().equals(ACTION_AUTO_UPDATE)) {
+ AppWidgetManager.getInstance(context).updateAppWidget(new ComponentName(context, BukaAnalyticsAppWidgetProvider.class), new RemoteViews(context.getPackageName(), R.layout.widget));
+ }
+ }
+
+ @Override
+ public void onEnabled(Context context) {
+ WidgetAlarm widgetAlarm = new WidgetAlarm(context);
+ widgetAlarm.startAlarm();
+ }
+
+ @Override
+ public void onDisabled(Context context) {
+ WidgetAlarm widgetAlarm = new WidgetAlarm(context);
+ widgetAlarm.stopAlarm();
+ }
+
+ private void getUnread(Context context, final AppWidgetManager appWidgetManager, final RemoteViews remoteViews, final int widgetId) {
+ HTTPRequestHelper helper = new HTTPRequestHelper();
+ helper.getAsJSONObject("https://api.bukalapak.com/v2/notifications/unreads.json", context.getApplicationContext(), new HTTPRequestHelper.JSONObjectCallback() {
+ @Override
+ public void onCompleted(Exception e, JsonObject result) {
+ String needAction = result.get("transactions_need_action_as_seller").getAsString();
+ String unreadMsg = result.get("messages").getAsString();
+
+ remoteViews.setTextViewText(R.id.textView_content_need_action, needAction);
+ remoteViews.setTextViewText(R.id.textView_content_unread_msg, unreadMsg);
+
+ appWidgetManager.updateAppWidget(widgetId, remoteViews);
+ }
+ });
+ }
+
+ private void getNotifByType(final String type, Context context, final AppWidgetManager appWidgetManager, final RemoteViews remoteViews, final int widgetId) {
+ HTTPRequestHelper helper = new HTTPRequestHelper();
+ String url = "https://api.bukalapak.com/v2/notifications/list.json?type[]=" + type;
+ String url2 = "https://api.bukalapak.com/v2/notifications/list.json?type[]=nego";
+ helper.getAsJSONObject(url2, context.getApplicationContext(), new HTTPRequestHelper.JSONObjectCallback() {
+ @Override
+ public void onCompleted(Exception e, JsonObject result) {
+ JsonArray data = result.get("data").getAsJsonArray();
+ String statStr = String.valueOf(data.size());
+
+ if (type.equals("report")) {
+ remoteViews.setTextViewText(R.id.textView_content_complaint, statStr);
+ } else if (type.equals("nego")) {
+ remoteViews.setTextViewText(R.id.textView_content_nego, statStr);
+ }
+
+ appWidgetManager.updateAppWidget(widgetId, remoteViews);
+ }
+ });
+ }
}
diff --git a/app/src/main/java/com/github/bukaanalytics/widget/widgetalarm/WidgetAlarm.java b/app/src/main/java/com/github/bukaanalytics/widget/widgetalarm/WidgetAlarm.java
new file mode 100644
index 0000000..f45f04d
--- /dev/null
+++ b/app/src/main/java/com/github/bukaanalytics/widget/widgetalarm/WidgetAlarm.java
@@ -0,0 +1,53 @@
+package com.github.bukaanalytics.widget.widgetalarm;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+
+import com.github.bukaanalytics.common.BukaAnalyticsBroadcastReceiver;
+import com.github.bukaanalytics.widget.BukaAnalyticsAppWidgetProvider;
+
+/**
+ * Created by touchtenmac-19 on 5/26/17.
+ */
+
+public class WidgetAlarm {
+ private final int ALARM_ID = 0;
+ private final int INTERVAL_MILLIS = 10000;
+
+ private Context mContext;
+
+
+ public WidgetAlarm(Context context)
+ {
+ mContext = context;
+ }
+
+
+ public void startAlarm()
+ {
+ // Construct an intent that will execute the AlarmReceiver
+ Intent intent = new Intent(BukaAnalyticsAppWidgetProvider.ACTION_AUTO_UPDATE);
+ // Create a PendingIntent to be triggered when the alarm goes off
+ final PendingIntent pIntent = PendingIntent.getBroadcast(
+ mContext, ALARM_ID, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+ // Setup periodic alarm every every half hour from this point onwards
+ long firstMillis = System.currentTimeMillis(); // alarm is set right away
+ AlarmManager alarm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+ // First parameter is the type: ELAPSED_REALTIME, ELAPSED_REALTIME_WAKEUP, RTC_WAKEUP
+ // Interval can be INTERVAL_FIFTEEN_MINUTES, INTERVAL_HALF_HOUR, INTERVAL_HOUR, INTERVAL_DAY
+ alarm.setInexactRepeating(AlarmManager.RTC_WAKEUP, firstMillis,
+ 1000 * 60 * 5, pIntent);
+ }
+
+
+ public void stopAlarm()
+ {
+ Intent alarmIntent = new Intent(BukaAnalyticsAppWidgetProvider.ACTION_AUTO_UPDATE);
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, ALARM_ID, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+
+ AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+ alarmManager.cancel(pendingIntent);
+ }
+}
diff --git a/app/src/main/res/drawable/border.xml b/app/src/main/res/drawable/border.xml
new file mode 100644
index 0000000..54b609b
--- /dev/null
+++ b/app/src/main/res/drawable/border.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index 1c29215..0d9eea1 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -10,9 +10,10 @@
diff --git a/app/src/main/res/layout/widget.xml b/app/src/main/res/layout/widget.xml
index 2a18d78..0b7b6d5 100644
--- a/app/src/main/res/layout/widget.xml
+++ b/app/src/main/res/layout/widget.xml
@@ -1,22 +1,152 @@
>
+ android:layout_height="wrap_content"
+ android:background="#55D71149">
+ android:textSize="12sp"
+ android:textColor="#FFFFFF"
+ android:text="Please login to get your stats" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml
index dd3e2a1..1c8c224 100644
--- a/app/src/main/res/menu/activity_main_drawer.xml
+++ b/app/src/main/res/menu/activity_main_drawer.xml
@@ -14,6 +14,10 @@
android:id="@+id/nav_bidding"
android:icon="@drawable/ic_bukaanalytics_bidding"
android:title="Bidding" />
+
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
index cde69bc..c5e9476 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
index c133a0c..4597f6c 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index bfa42f0..488ea49 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 324e72c..4e12aa3 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index aee44e1..5594d84 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/xml/widget_info.xml b/app/src/main/res/xml/widget_info.xml
index 597decd..edd698b 100644
--- a/app/src/main/res/xml/widget_info.xml
+++ b/app/src/main/res/xml/widget_info.xml
@@ -1,7 +1,7 @@
{
+ const locals = super.getLocals();
+ const { config: { elements, propForQuery } } = locals;
+ const query = this.props.value;
+ this.setState({ elements, propForQuery, query });
+ locals.onChange(query);
+ });
+ }
+
+ getLocals() {
+ return super.getLocals();
+ }
+
+ getTemplate() {
+ return function (locals) {
+ const { query } = this.state;
+ const elements = this.findElement(query);
+ const comp = (a, b) => a.toLowerCase().trim() === b.toLowerCase().trim();
+
+ const stylesheet = locals.stylesheet;
+ const formGroupStyle = stylesheet.formGroup.normal;
+ const controlLabelStyle = stylesheet.controlLabel.normal;
+ const textboxStyle = stylesheet.textbox.normal;
+
+ const label = locals.label ? {locals.label} : null;
+ return (
+
+ {label}
+
+ {
+ this.setState({ query: text });
+ locals.onChange(text);
+ }}
+ renderItem={( item ) => (
+ {
+ this.setState({ query: item });
+ locals.onChange(item);
+ }}
+ >
+ {item}
+
+ )}
+ />
+
+
+ {elements.length > 0 ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ );
+ }.bind(this);
+ }
+
+ findElement(query) {
+ if (query === '' || !query) {
+ return [];
+ }
+
+ const { elements } = this.state;
+ const regex = new RegExp(`${query.trim()}`, 'i');
+ return elements.filter(element => element.search(regex) >= 0).slice(0, 4);
+ }
+}
+
+// const styles = {
+// container: {
+//
+// // backgroundColor: '#F5FCFF',
+// },
+// autocompleteContainer: {
+// flex: 1,
+// left: 0,
+// position: 'absolute',
+// right: 0,
+// top: 0,
+// zIndex: 1
+// },
+// inputContainerStyle: {
+// borderRadius: 4,
+// borderColor: '#cccccc',
+// borderWidth: 1,
+// marginBottom: 18,
+// },
+// itemText: {
+// fontSize: 15,
+// margin: 2,
+// },
+// };
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingTop: 25,
+ },
+ autocompleteContainer: {
+ flex: 1,
+ left: AppSizes.screen.widthThird,
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ zIndex: 1,
+ },
+ itemText: {
+ fontSize: 15,
+ margin: 2,
+ },
+ descriptionContainer: {
+ // `backgroundColor` needs to be set otherwise the
+ // autocomplete input will disappear on text input.
+ marginTop: 25,
+ },
+ infoText: {
+ textAlign: 'center',
+ },
+ titleText: {
+ fontSize: 18,
+ fontWeight: '500',
+ marginBottom: 10,
+ marginTop: 10,
+ textAlign: 'center',
+ },
+ directorText: {
+ color: 'grey',
+ fontSize: 12,
+ marginBottom: 10,
+ textAlign: 'center',
+ },
+ openingText: {
+ textAlign: 'center',
+ },
+ inputContainerStyle: {
+ borderRadius: 0,
+ borderColor: '#cccccc',
+ borderWidth: 0,
+ marginBottom: 5,
+ height: 36,
+ width: AppSizes.screen.widthTwoThirds,
+ },
+});
+
+AutoInput.transformer = {
+ format: value => {
+ return value;
+ },
+ parse: value => {
+ return value;
+ },
+};
+
+export default AutoInput;
diff --git a/js-src/components/ui/FormInput.js b/js-src/components/ui/FormInput.js
index 71db4bb..70c378b 100644
--- a/js-src/components/ui/FormInput.js
+++ b/js-src/components/ui/FormInput.js
@@ -45,7 +45,7 @@ class CustomFormInput extends Component {
inputStyle: [{
color: AppColors.textPrimary,
fontFamily: AppFonts.base.family,
- paddingHorizontal: 10,
+ paddingHorizontal: 0,
paddingVertical: 3,
}],
};
diff --git a/js-src/index.js b/js-src/index.js
index 22d58b2..d513386 100644
--- a/js-src/index.js
+++ b/js-src/index.js
@@ -1,9 +1,10 @@
import React, { Component } from 'react'
-import { StyleSheet, Text, View } from 'react-native'
+import { AsyncStorage, StyleSheet, Text, View } from 'react-native'
import { applyMiddleware, compose, createStore } from 'redux';
import { connect, Provider } from 'react-redux';
import { composeWithDevTools } from 'remote-redux-devtools';
import { createLogger } from 'redux-logger';
+import { persistStore, autoRehydrate } from 'redux-persist'
import thunk from 'redux-thunk';
// All redux reducers (rolled into one mega-reducer)
@@ -33,7 +34,10 @@ class Root extends Component {
const composeEnhancers = composeWithDevTools({ realtime: true, port: 8000 });
this.store = composeEnhancers(
applyMiddleware(...middleware),
+ autoRehydrate()
)(createStore)(rootReducer)
+
+ persistStore(this.store, { storage: AsyncStorage })
}
render() {
diff --git a/js-src/lib/BLApi.js b/js-src/lib/BLApi.js
new file mode 100644
index 0000000..232c723
--- /dev/null
+++ b/js-src/lib/BLApi.js
@@ -0,0 +1,246 @@
+import axios from 'axios';
+import { ToastAndroid } from 'react-native'
+
+const API_PRODUCTS = 'https://api.bukalapak.com/v2/products.json';
+const API_BASE_URL = 'https://api.bukalapak.com/v2'
+
+class BLApi {
+ // =============================
+ // Api call related functions
+ // =============================
+ static sendApiRequest(config, onSuccess, onError) {
+ return axios(config).then(res => {
+ console.log(res)
+ switch (res.status) {
+ case 200:
+ return res.data
+ break;
+ case 410: // Gone – Consider updating your client application.
+ throw Error("Data unavailable due to unupdated application")
+ break;
+ case 500:
+ throw Error("Internal Server Error – We had a problem with our server. Try again later.")
+ break;
+ default:
+ throw Error("Unknown Error")
+ break;
+ }
+ })
+ .then(data => {
+ if (data.status != 'OK') throw Error(data.message)
+ else onSuccess(data)
+ })
+ .catch(err => {
+ onError(err)
+ ToastAndroid.show(err.message, ToastAndroid.LONG)
+ })
+ }
+
+ static authenticateUser(data, successCallback, errorCallback) {
+ return this.sendApiRequest({
+ method: 'post',
+ url: API_BASE_URL + '/authenticate.json',
+ auth: { username: data.username, password: data.password },
+ }, resData => successCallback(resData), err => errorCallback(err))
+ }
+
+ static getTransactions(params, successCallback, errorCallback) {
+ let { userId, token, perPage, page, since } = params
+
+ return this.sendApiRequest({
+ method: 'get',
+ url: 'https://api.bukalapak.com/v2/transactions.json',
+ auth: {
+ username: userId,
+ password: token
+ },
+ params: {
+ page: page,
+ per_page: perPage,
+ seller: 1,
+ status: 5,
+ created_since: since
+ }
+ }, resData => {
+ successCallback(resData.transactions)
+
+ if (resData.transactions.length == perPage) {
+ let newParam = {
+ userId, token, perPage, since,
+ page: page+1
+ }
+ this.getTransactions(newParam, successCallback, errorCallback)
+ }
+ }, err => errorCallback(err))
+ }
+
+ static getProducts(page, keyword, filter = {}) {
+ console.log("selected filter are : " , filter);
+ return axios.get(API_PRODUCTS, {
+ params: {
+ page: page || 1,
+ per_page: 24,
+ keywords: keyword || 'murah',
+ category_id: filter.category_id || 159,
+ nego: filter.nego || 1,
+ harga_pas: filter.harga_pas || 1,
+ province: filter.province || 'DKI Jakarta',
+ top_seller: filter.top_seller || 0,
+ city: filter.city || 'Jakarta Pusat',
+ price_min: filter.price_min || 0,
+ price_max: filter.price_max || 999999999,
+ sort_by: filter.sort_by || 'Termurah', // Termahal, Terbaru, Acak
+ },
+ });
+ }
+
+ // =============================
+ // Static functions
+ // =============================
+
+ static mathAnalysis(result) {
+ if ( result != null && result.length > 0 ) {
+ const prices = result.sort((a, b) => {
+ return a.price - b.price;
+ });
+
+ const max_price = prices[prices.length - 1].price;
+ const min_price = prices[0].price;
+ let avg_price = 0;
+
+ const num_class_interval = Math.round(1 + 3.3 * Math.log10(prices.length));
+ const range = max_price - min_price;
+ console.log(this);
+ const summary = this.initSummary(num_class_interval);
+
+ for (let i = prices.length - 1; i >= 0; i--) {
+ avg_price = avg_price + prices[i].price;
+ prices[i].class = this.checkClassRange(prices[i].price, min_price, range, num_class_interval);
+
+ const text = `_${prices[i].class}`;
+
+ summary[text].avg_price = summary[text].avg_price + prices[i].price;
+ summary[text].avg_sold = summary[text].avg_sold + prices[i].sold;
+ summary[text].count = summary[text].count + 1;
+ }
+
+ const profit = {};
+ for (let i = num_class_interval - 1; i >= 0; i--) {
+ const text = `_${i + 1}`;
+ // safe division by zero.. not to trigger NaN
+ summary[text].avg_price = (summary[text].count > 0 ? summary[text].avg_price / summary[text].count : 0);
+ summary[text].avg_sold = (summary[text].count > 0 ? summary[text].avg_sold / summary[text].count : 0);
+
+ profit[text] = summary[text].avg_price * summary[text].avg_sold;
+ }
+
+ // find best price
+ const best_price_index = this.findBestPrice(profit);
+ const best_price = summary[best_price_index].avg_price;
+
+ //generategraphdata
+ const graph = this.generateGraphData(summary, num_class_interval);
+
+ // calculate avg_price
+ avg_price = avg_price / prices.length;
+
+ const retval = {
+ min_price,
+ max_price,
+ avg_price,
+ best_price,
+ summary,
+ graph,
+ };
+ return retval;
+
+ }else{
+
+ const retval = {
+ min_price : 0,
+ max_price : 0,
+ avg_price : 0,
+ best_price : 0,
+ summary : null,
+ graph: [],
+ };
+ return retval;
+ }
+ }
+
+ // =============================
+ // Private functions
+ // =============================
+ static parsePrice(succ) {
+ const prices = [];
+ if (succ.products) {
+ const products = succ.products;
+ for (let i = products.length - 1; i >= 0; i--) {
+ prices.push({
+ price: products[i].price,
+ sold: products[i].sold_count,
+ class: '',
+ });
+ }
+ }
+ return prices;
+ }
+
+ static checkClassRange(value, min_value, range_max_min, num_class) {
+ const percentage = (value - min_value) / range_max_min;
+ for (let i = num_class - 1; i >= 0; i--) {
+ const j = i + 1;
+ const lower_limit = i / num_class;
+ const upper_limit = j / num_class;
+ if (lower_limit <= percentage && percentage < upper_limit) {
+ return j;
+ }
+ }
+ return num_class;
+ }
+
+ static initSummary(num_class) {
+ const summary = {};
+ for (let i = 1; i < num_class + 1; i++) {
+ const text = `_${i}`;
+ summary[text] = {
+ avg_price: 0,
+ avg_sold: 0,
+ count: 0,
+ };
+ }
+ return summary;
+ }
+
+ static findBestPrice(profit) {
+ let first_time = true;
+ let max_key = null;
+ for (const key in profit) {
+ if (profit.hasOwnProperty(key)) {
+ if (first_time) {
+ max_key = key;
+ first_time = false;
+ }
+
+ if (profit[key] > profit[max_key]) {
+ max_key = key;
+ }
+ }
+ }
+ return max_key;
+ }
+
+ static generateGraphData(summary, num_class) {
+ const graph = [];
+ for (let i = 0; i < num_class; i++) {
+ const text = `_${i + 1}`;
+ graph.push({
+ name: text,
+ v: summary[text].count,
+ });
+ }
+ return graph;
+ }
+}
+
+export default BLApi;
diff --git a/js-src/lib/BLSqlite.js b/js-src/lib/BLSqlite.js
new file mode 100644
index 0000000..7b4d663
--- /dev/null
+++ b/js-src/lib/BLSqlite.js
@@ -0,0 +1,339 @@
+import SQLite from 'react-native-sqlite-storage';
+
+const config = {
+ dbname: 'baDatabase',
+ dbversion: 1,
+ tables: {
+ users: 'users',
+ products: 'products',
+ stats: 'stats',
+ tokens: 'tokens'
+ },
+ col: {
+ users: {
+ id: 'seller_id',
+ name: 'name',
+ },
+ products: {
+ id: 'product_id',
+ name: 'name',
+ price: 'price',
+ seller_id: 'seller_id',
+ },
+ stats: {
+ id: 'id',
+ date: 'date',
+ day_name: 'day_name',
+ product_id: 'product_id',
+ view_count: 'view_count',
+ view_total: 'view_total',
+ sold_count: 'sold_count',
+ sold_total: 'sold_total',
+ interest_count: 'interest_count',
+ interest_total: 'interest_total',
+ },
+ tokens: {
+ id: 'id',
+ user_id: 'user_id',
+ token: 'token'
+ }
+ },
+};
+
+
+class BLSqlite {
+
+ constructor() {
+ this.openDatabase();
+ this.createTablesIfNotExists();
+ }
+
+ openDatabase() {
+ this.db = SQLite.openDatabase({
+ name: config.dbname,
+ }, null);
+ }
+
+ getWeeklyView(param, callback) {
+ console.log("Weekly view is ");
+ console.log(param);
+ this.db.executeSql(`
+ SELECT
+ ${config.col.stats.day_name} as day_name,
+ SUM(${config.col.stats.view_count}) as daily_view
+ FROM ${config.tables.stats}
+ WHERE ${config.col.stats.date} BETWEEN ? AND ?
+ GROUP BY ${config.col.stats.day_name}
+ `, [param.start_date, param.end_date], results => {
+ console.log("results inside executeSql");
+ console.log(results);
+ callback(this.parseWeeklyViewResult(results));
+ });
+ }
+
+ getWeeklyRevenueAttribution(param, callback) {
+ this.db.executeSql(`
+ SELECT name,
+ ( terjual * price ) AS revenue
+ FROM (SELECT product_id,
+ SUM(sold_count) AS terjual
+ FROM stats
+ WHERE DATE BETWEEN ? AND ?
+ GROUP BY product_id) AS A
+ JOIN products AS B
+ ON A.product_id = B.product_id
+ ORDER BY revenue DESC
+ LIMIT 5
+ `, [param.start_date, param.end_date], results => {
+ callback(this.parseWeeklyRevenueAttribution(results));
+ });
+ }
+
+ getWeeklyTopViewedProduct(param, callback) {
+ this.db.executeSql(`
+ SELECT name,
+ view
+ FROM (SELECT product_id,
+ SUM(view_count) AS view
+ FROM stats
+ WHERE DATE BETWEEN ? AND ?
+ GROUP BY product_id) AS A
+ JOIN products AS B
+ ON A.product_id = B.product_id
+ ORDER BY view DESC
+ LIMIT 5
+ `, [param.start_date, param.end_date], results => {
+ callback(this.parseWeeklyViewProduct(results));
+ });
+ }
+
+ getWeeklyLeastViewedProduct(param, callback) {
+ this.db.executeSql(`
+ SELECT name,
+ view
+ FROM (SELECT product_id,
+ SUM(view_count) AS view
+ FROM stats
+ WHERE DATE BETWEEN ? AND ?
+ GROUP BY product_id) AS A
+ JOIN products AS B
+ ON A.product_id = B.product_id
+ ORDER BY view ASC
+ LIMIT 5
+ `, [param.start_date, param.end_date], results => {
+ callback(this.parseWeeklyViewProduct(results));
+ });
+ }
+
+ getWeeklyConvertionRate(param, callback) {
+ this.db.executeSql(`
+ SELECT SUM(${config.col.stats.sold_count}) AS sold_accumulated,
+ SUM(${config.col.stats.view_count}) AS view_accumulated
+ FROM ${config.tables.stats}
+ WHERE DATE BETWEEN ? AND ?
+ `, [param.start_date, param.end_date], results => {
+ callback(this.parseWeeklyConvertionRate(results));
+ });
+ }
+
+ getBidSuggestion(param, callback) {
+ console.log('param is :',param );
+ this.db.executeSql(`
+ SELECT name,
+ ( ratio * price * 0.1 ) AS bid_suggestion
+ FROM (SELECT with_ads.product_id,
+ ( CASE
+ WHEN with_ads.view_iklan <= without_ads.VIEW THEN -1
+ WHEN with_ads.sold_iklan <= without_ads.sold THEN -1
+ ELSE ( ( with_ads.sold_iklan - without_ads.sold ) /
+ ( with_ads.view_iklan -
+ without_ads.VIEW ) )
+ END ) AS ratio
+ FROM (SELECT Avg(view_count) AS view_iklan,
+ Avg(sold_count) AS sold_iklan,
+ product_id
+ FROM stats
+ WHERE date BETWEEN ? AND ?
+ GROUP BY product_id) AS with_ads
+ JOIN (SELECT Avg(view_count) AS VIEW,
+ Avg(sold_count) AS sold,
+ product_id
+ FROM stats
+ WHERE date BETWEEN ? AND ?
+ GROUP BY product_id) AS without_ads
+ ON with_ads.product_id = without_ads.product_id
+ WHERE ratio > 0) AS A
+ JOIN products AS B
+ ON A.product_id = B.product_id
+ `, [param.start_ads, param.end_ads, param.start_no_ads, param.end_no_ads], results => {
+ callback(this.parseBidSuggestion(results));
+ });
+ }
+
+ getActiveToken(callback) {
+ this.db.executeSql(`
+ SELECT * from tokens
+ ORDER BY id DESC
+ LIMIT 1
+ `, [], result => {
+ callback(this.parseActiveToken(result))
+ })
+ }
+
+ createTablesIfNotExists() {
+ this.db.executeSql(`
+ CREATE TABLE IF NOT EXISTS users (
+ seller_id INTEGER PRIMARY KEY,
+ name TEXT
+ )`);
+ this.db.executeSql(`
+ CREATE TABLE IF NOT EXISTS products(
+ product_id TEXT PRIMARY KEY,
+ name TEXT,
+ price INTEGER,
+ seller_id INTEGER,
+ FOREIGN KEY(seller_id) REFERENCES users(seller_id)
+ )`);
+ this.db.executeSql(`
+ CREATE TABLE IF NOT EXISTS products(
+ id INTEGER PRIMARY KEY,
+ date TEXT,
+ day_name TEXT,
+ product_id TEXT,
+ view_count INTEGER,
+ view_total INTEGER,
+ sold_count INTEGER,
+ sold_total INTEGER,
+ interest_count INTEGER,
+ interest_total INTEGER
+ )`);
+ this.db.executeSql(`
+ CREATE TABLE IF NOT EXISTS tokens(
+ id INTEGER PRIMARY KEY,
+ user_id INTEGER,
+ token TEXT
+ )`);
+ }
+
+ insertUser(param) {
+ this.db.executeSql(`
+ INSERT INTO users (
+ seller_id,
+ name
+ ) VALUES (?, ?)
+ `, [param.user_id, param.name]);
+ }
+
+ insertProducts(param) {
+ this.db.executeSql(`
+ INSERT INTO users (
+ product_id,
+ name,
+ price,
+ seller_id
+ ) VALUES (?, ?, ?, ?)
+ `, [param.product_id, param.name, param.price, param.seller_id]);
+ }
+
+ insertStats(param) {
+ this.db.executeSql(`
+ INSERT INTO users (
+ date,
+ day_name,
+ product_id,
+ view_count,
+ view_total,
+ sold_count,
+ sold_total,
+ interest_count,
+ interest_total
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `, [param.date, param.day_name, param.product_id, param.view_count, param.view_total, param.sold_count, param.sold_total, param.interest_count, param.interest_total]);
+ }
+
+ insertToken(param) {
+ this.db.executeSql(`
+ INSERT INTO ${config.tables.tokens} (
+ ${config.col.tokens.user_id},
+ ${config.col.tokens.token}
+ ) VALUES (?, ?)
+ `, [param.userId, param.token]);
+ }
+
+ // ==================
+ // Private Functions
+ // ==================
+ parseWeeklyViewResult(results) {
+ const retval = {};
+ const result_length = results.rows.length;
+ for (let i = 0; i < result_length; i++) {
+ const row = results.rows.item(i);
+ retval[`${row.day_name}`] = row.daily_view;
+ }
+ return retval;
+ }
+
+ parseWeeklyRevenueAttribution(results) {
+ const retval = [];
+ const result_length = results.rows.length;
+ for (let i = 0; i < result_length; i++) {
+ const row = results.rows.item(i);
+ retval.push({
+ name: row.name,
+ attribution: row.revenue,
+ });
+ }
+ return retval;
+ }
+
+ parseWeeklyViewProduct(results) {
+ const retval = [];
+ const result_length = results.rows.length;
+ for (let i = 0; i < result_length; i++) {
+ const row = results.rows.item(i);
+ retval.push({
+ item: row.name,
+ view: row.view,
+ });
+ }
+ return retval;
+ }
+
+ parseWeeklyConvertionRate(results) {
+ const retval = {};
+ const result_length = results.rows.length;
+ for (let i = 0; i < result_length; i++) {
+ const row = results.rows.item(i);
+ retval.view_accumulated = row.view_accumulated;
+ retval.sold_accumulated = row.sold_accumulated;
+ }
+ return retval;
+ }
+
+ parseBidSuggestion(results) {
+ const retval = [];
+ const result_length = results.rows.length;
+ for (let i = 0; i < result_length; i++) {
+ const row = results.rows.item(i);
+ retval.push({
+ item: row.name,
+ bid_suggestion: row.bid_suggestion,
+ });
+ }
+ return retval;
+ }
+
+ parseActiveToken(result) {
+ const retval = {}
+ const result_length = results.rows.length;
+ for (let i = 0; i < result_length; i++) {
+ const row = results.rows.item(i);
+ retval.userId = row.user_id;
+ retval.token = row.token;
+ }
+ return retval;
+ }
+
+}
+
+export const Sqlite = new BLSqlite();
diff --git a/js-src/navigation/NavOptions.js b/js-src/navigation/NavOptions.js
index 5298c7b..cb9215b 100644
--- a/js-src/navigation/NavOptions.js
+++ b/js-src/navigation/NavOptions.js
@@ -11,7 +11,7 @@ const navOptions = ({navigation}) => ({
navigation.navigate('DrawerOpen')}
style={{justifyContent: 'center', alignItems: 'center', padding: 8, paddingTop: 12}}>
-
+
)
})
diff --git a/js-src/navigation/PricingAnalysisStack.js b/js-src/navigation/PricingAnalysisStack.js
index 3292a5d..43381c0 100644
--- a/js-src/navigation/PricingAnalysisStack.js
+++ b/js-src/navigation/PricingAnalysisStack.js
@@ -1,13 +1,17 @@
-import { StackNavigator } from 'react-navigation'
-import PricingAnalysisScreen from '@screen/PricingAnalysis/PricingAnalysisContainer'
-import navOptions from '@nav/NavOptions'
+import { StackNavigator } from 'react-navigation';
+import PricingAnalysisScreen from '@screen/PricingAnalysis/PricingAnalysisContainer';
+import PricingFilterScreen from '@screen/PricingFilter/PricingFilterContainer';
+import navOptions from '@nav/NavOptions';
+import { AppColors } from '@theme';
const PricingAnalysisStack = StackNavigator({
PricingAnalysis: {
- screen: PricingAnalysisScreen
- }
-}, {
- navigationOptions: navOptions
-})
+ screen: PricingAnalysisScreen,
+ navigationOptions: navOptions,
+ },
+ FilterScreen: {
+ screen: PricingFilterScreen,
+ },
+});
-export default PricingAnalysisStack
\ No newline at end of file
+export default PricingAnalysisStack;
diff --git a/js-src/redux/bidding/actions.js b/js-src/redux/bidding/actions.js
new file mode 100644
index 0000000..f59d6c2
--- /dev/null
+++ b/js-src/redux/bidding/actions.js
@@ -0,0 +1,36 @@
+import moment from 'moment';
+import { Sqlite } from '../../lib/BLSqlite';
+import {
+ BIDDING_ACTION_SET,
+} from './constant';
+
+// Sementara ini asumsi no_ads = 2 minggu lalu, with ads = 1 minggu lalu
+export function refreshBiddingData() {
+ return (dispatch, getState) => {
+ const latestDate = getState().dashboard.latestDate;
+ // diganti dulu ya .. sebelumnya ada kontrol
+ const momentObj = moment(); //moment(latestDate, 'X');
+
+ const end_ads = moment(momentObj, 'X').format('YYYY-MM-DD');
+ const start_ads = moment(momentObj, 'X').day(1).format('YYYY-MM-DD');
+ const end_no_ads = moment(momentObj, 'X').day(0).format('YYYY-MM-DD');
+ const start_no_ads = moment(momentObj, 'X').day(-6).format('YYYY-MM-DD');
+
+ Sqlite.getBidSuggestion({
+ start_ads: start_ads,
+ end_ads: end_ads,
+ start_no_ads: start_no_ads,
+ end_no_ads: end_no_ads,
+ }, (res) => {
+ dispatch(setData('bid_suggestion', res));
+ });
+ };
+}
+
+function setData(key, value) {
+ return {
+ type: BIDDING_ACTION_SET,
+ key,
+ value,
+ };
+}
diff --git a/js-src/redux/bidding/constant.js b/js-src/redux/bidding/constant.js
new file mode 100644
index 0000000..4b8ab4c
--- /dev/null
+++ b/js-src/redux/bidding/constant.js
@@ -0,0 +1 @@
+export const BIDDING_ACTION_SET = 'BIDDING_ACTION_SET';
diff --git a/js-src/redux/bidding/reducer.js b/js-src/redux/bidding/reducer.js
new file mode 100644
index 0000000..8d8ac9c
--- /dev/null
+++ b/js-src/redux/bidding/reducer.js
@@ -0,0 +1,20 @@
+import {
+ BIDDING_ACTION_SET,
+} from './constant';
+import moment from 'moment'
+
+const initialState = {
+ bid_suggestion: [],
+};
+
+export default function biddingReducer(state = initialState, action) {
+ switch (action.type) {
+ case BIDDING_ACTION_SET:
+ return {
+ ...state,
+ [action.key]: action.value,
+ };
+ default:
+ return state;
+ }
+}
diff --git a/js-src/redux/dashboard/actions.js b/js-src/redux/dashboard/actions.js
new file mode 100644
index 0000000..06c9afc
--- /dev/null
+++ b/js-src/redux/dashboard/actions.js
@@ -0,0 +1,168 @@
+import BLApi from '@lib/BLApi'
+import moment from 'moment';
+import { Sqlite } from '../../lib/BLSqlite';
+export const DASHBOARD = {
+ TO_NEXT_WEEK: 'DASHBOARD_TO_NEXT_WEEK',
+ TO_PREV_WEEK: 'DASHBOARD_TO_PREV_WEEK',
+ GET_DASHBOARD_DATA: 'DASHBOARD_GET_DATA',
+ SET_DASHBOARD_DATA: 'DASHBOARD_SET_DATA',
+ SET_TRANSACTION_DATA: 'DASHBOARD_SET_TRANSACTION_DATA',
+ RESET_TRANSACTION_DATA: 'DASHBOARD_RESET_TRANSACTION_DATA',
+ FETCHING_TRANSACTION_DATA: 'DASHBOARD_FETCHING_TRANSACTION_DATA',
+ FETCH_TRANSACTION_COMPLETED: 'DASHBOARD_FETCH_TRANSACTION_COMPLETED',
+ FETCHING_SQLITE_DATA: 'DASHBOARD_FETCHING_SQLITE_DATA',
+};
+
+export function refreshData(refreshType) {
+ let perPage = 1
+
+ return function(dispatch, getState) {
+ dispatch(fetchingTransactionData())
+ dispatch(resetTransactionData())
+
+ if (refreshType == 'next') {
+ dispatch(setDateToNextWeek());
+ } else if (refreshType == 'prev') {
+ dispatch(setDateToPrevWeek());
+ }
+
+ let latestDate = getState().dashboard.latestDate
+ let since = moment(latestDate, 'X').subtract(13, 'day').format('YYYY-MM-DD')
+
+ dispatch(getSqliteData(latestDate));
+ console.log(Sqlite);
+
+ let userData = getState().user
+
+ return BLApi.getTransactions({
+ userId: userData.userId,
+ token: userData.token,
+ perPage: perPage,
+ page: 1,
+ since: since
+ }, transactions => {
+ dispatch(setTransactionData(transactions))
+ if (transactions.length < perPage) dispatch(fetchTransactionCompleted())
+ }, err => {
+ console.log(err)
+ })
+
+ }
+}
+
+function fetchingTransactionData() {
+ return {
+ type: DASHBOARD.FETCHING_TRANSACTION_DATA
+ }
+}
+
+function fetchTransactionCompleted() {
+ return {
+ type: DASHBOARD.FETCH_TRANSACTION_COMPLETED
+ }
+}
+
+function resetTransactionData() {
+ return {
+ type: DASHBOARD.RESET_TRANSACTION_DATA
+ }
+}
+
+function setTransactionData(transactions) {
+ return {
+ type: DASHBOARD.SET_TRANSACTION_DATA,
+ payload: { transactions }
+ }
+}
+
+function setDateToNextWeek() {
+ return {
+ type: DASHBOARD.TO_NEXT_WEEK
+ }
+}
+
+function setDateToPrevWeek() {
+ return {
+ type: DASHBOARD.TO_PREV_WEEK
+ }
+}
+
+function setData(key, value) {
+ return {
+ type: DASHBOARD.SET_DASHBOARD_DATA,
+ key,
+ value,
+ };
+}
+
+function fetchSqliteData(flag) {
+ return {
+ type: DASHBOARD.FETCHING_SQLITE_DATA,
+ flag,
+ };
+}
+
+function getSqliteData(latestDate) {
+ const momentObj = moment(latestDate, 'X');
+ const end_date = momentObj.format('YYYY-MM-DD');
+ const start_date = momentObj.day(-6).format('YYYY-MM-DD');
+
+ const end_prev_week = momentObj.subtract(1, 'day').format('YYYY-MM-DD');
+ const start_prev_week = momentObj.subtract(6, 'day').format('YYYY-MM-DD');
+ console.log("end_prev_week", end_prev_week, "start_next week", start_prev_week);
+ return dispatch => {
+ dispatch(fetchSqliteData(true));
+ Sqlite.getWeeklyView({
+ start_date: start_date,
+ end_date: end_date,
+ }, (res) => {
+ dispatch(setData('weekly_view', res));
+ });
+
+ Sqlite.getWeeklyLeastViewedProduct({
+ start_date: start_date,
+ end_date: end_date,
+ }, (res) => {
+ dispatch(setData('least_viewed', res));
+ });
+
+ Sqlite.getWeeklyTopViewedProduct({
+ start_date: start_date,
+ end_date: end_date,
+ }, (res) => {
+ dispatch(setData('most_viewed', res));
+ });
+
+ Sqlite.getWeeklyConvertionRate({
+ start_date: start_date,
+ end_date: end_date,
+ }, (res) => {
+ dispatch(setData('convertion_rate', res));
+ });
+
+ Sqlite.getWeeklyConvertionRate({
+ start_date: start_prev_week,
+ end_date: end_prev_week,
+ }, (res) => {
+ dispatch(setData('prev_convertion_rate', res));
+ });
+
+ Sqlite.getWeeklyRevenueAttribution({
+ start_date: start_date,
+ end_date: end_date,
+ }, (res) => {
+ dispatch(setData('revenue_attribution', res));
+ });
+
+ dispatch(fetchSqliteData(false));
+ };
+}
+
+function getDateRange(getState) {
+ const latestDate = getState().dashboard.latestDate;
+ const momentObj = moment(latestDate, 'X');
+
+ const sundayDateStr = momentObj.format('YYYY-MM-DD');
+ const mondayDateStr = momentObj.day(-6).format('YYYY-MM-DD');
+ return { sundayDateStr, mondayDateStr };
+}
diff --git a/js-src/redux/dashboard/reducer.js b/js-src/redux/dashboard/reducer.js
new file mode 100644
index 0000000..f477d83
--- /dev/null
+++ b/js-src/redux/dashboard/reducer.js
@@ -0,0 +1,100 @@
+import { DASHBOARD } from '@redux/dashboard/actions'
+import moment from 'moment'
+
+const initialState = {
+ latestDate: moment().day(7).unix(),
+ rawTransactions: [],
+ fetchingTransaction: false,
+ currentRevenue: 0,
+ previousRevenue: 0,
+ weekly_view: {},
+ least_viewed: [],
+ most_viewed: [],
+ convertion_rate: 0,
+ prev_convertion_rate: 0,
+ revenue_attribution: [
+ {
+ name: '',
+ attribution: 1,
+ },
+ ],
+ isFetchingSqliteData: false,
+};
+
+export default function dashboardReducer(state = initialState, action) {
+ let newLatestDate
+ let newRawTransactions
+ let revenueObj
+
+ switch (action.type) {
+ case DASHBOARD.TO_NEXT_WEEK:
+ newLatestDate = moment(state.latestDate, 'X').add(7, 'day').unix()
+ return {
+ ...state,
+ latestDate: newLatestDate
+ }
+ case DASHBOARD.TO_PREV_WEEK:
+ newLatestDate = moment(state.latestDate, 'X').subtract(7, 'day').unix()
+ return {
+ ...state,
+ latestDate: newLatestDate
+ }
+
+ case DASHBOARD.SET_TRANSACTION_DATA:
+ newRawTransactions = state.rawTransactions.concat(action.payload.transactions)
+ let endOfWeek = moment(state.latestDate, 'X').format()
+ let startOfWeek = moment(endOfWeek).subtract(6, 'day').format()
+ let endOfPrevWeek = moment(startOfWeek).subtract(1, 'day').format()
+ let startOfPrevWeek = moment(endOfPrevWeek).subtract(6, 'day').format()
+
+ revenueObj = action.payload.transactions.reduce((prev, current) => {
+ let tempCR = prev.currentRevenue
+ let tempPR = prev.previousRevenue
+ if (current.state == 'remitted' && moment(current.created_at).isBetween(startOfWeek, endOfWeek, 'day')) tempCR += current.amount
+ if (current.state == 'remitted' && moment(current.created_at).isBetween(startOfPrevWeek, endOfPrevWeek, 'day')) tempPR += current.amount
+
+ return { currentRevenue: tempCR, previousRevenue: tempPR }
+ }, {
+ currentRevenue: state.currentRevenue,
+ previousRevenue: state.previousRevenue
+ })
+
+ return {
+ ...state,
+ ...revenueObj,
+ rawTransactions: newRawTransactions,
+ }
+
+ case DASHBOARD.RESET_TRANSACTION_DATA:
+ return {
+ ...state,
+ rawTransactions: [],
+ currentRevenue: 0,
+ previousRevenue: 0
+ }
+
+ case DASHBOARD.FETCHING_TRANSACTION_DATA:
+ return {
+ ...state,
+ fetchingTransaction: true
+ }
+
+ case DASHBOARD.FETCH_TRANSACTION_COMPLETED:
+ return {
+ ...state,
+ fetchingTransaction: false
+ }
+ case DASHBOARD.SET_DASHBOARD_DATA:
+ return {
+ ...state,
+ [action.key]: action.value,
+ };
+ case DASHBOARD.FETCHING_SQLITE_DATA:
+ return {
+ ...state,
+ isFetchingSqliteData: action.flag,
+ };
+ default:
+ return state;
+ }
+}
diff --git a/js-src/redux/index.js b/js-src/redux/index.js
index 3f8bb0d..8572ebd 100644
--- a/js-src/redux/index.js
+++ b/js-src/redux/index.js
@@ -1,12 +1,20 @@
import { combineReducers } from 'redux';
-import nav from '@redux/nav/reducer'
-import user from '@redux/user/reducer'
+import dashboard from '@redux/dashboard/reducer';
+import nav from '@redux/nav/reducer';
+import user from '@redux/user/reducer';
+import pricing from '@redux/pricing/reducer';
+import pricing_filter from '@redux/pricing_filter/reducer';
+import bidding from '@redux/bidding/reducer';
// Combine all
const appReducer = combineReducers({
+ dashboard,
nav,
- user
+ user,
+ pricing,
+ pricing_filter,
+ bidding,
});
// Setup root reducer
@@ -15,4 +23,4 @@ const rootReducer = (state, action) => {
return appReducer(newState, action);
};
-export default rootReducer;
\ No newline at end of file
+export default rootReducer;
diff --git a/js-src/redux/pricing/actions.js b/js-src/redux/pricing/actions.js
new file mode 100644
index 0000000..2a243b1
--- /dev/null
+++ b/js-src/redux/pricing/actions.js
@@ -0,0 +1,83 @@
+import axios from 'axios';
+import BLApi from '../../lib/BLApi';
+import {
+ PRICING_ACTION_SET,
+ PRICING_ACTION_FILTER,
+ PRICING_ACTION_FETCHING,
+ PRICING_ACTION_CALCULATING,
+} from './constant';
+// ini apa bib ? ntar tanyain ya .. pas hari h
+// User action types
+export const USER = [{
+ LOGIN: 'USER_LOGIN',
+}];
+
+// User action creator
+export function setData(data) {
+ return {
+ type: PRICING_ACTION_SET,
+ data,
+ };
+}
+
+export function fetching(flag) {
+ return {
+ type: PRICING_ACTION_FETCHING,
+ flag,
+ };
+}
+
+export function calculating(flag) {
+ return {
+ type: PRICING_ACTION_CALCULATING,
+ flag,
+ };
+}
+
+export function filter(filter) {
+ return {
+ type: PRICING_ACTION_FILTER,
+ filter,
+ };
+}
+
+export function getGraph(keyword, filter = null) {
+ return dispatch => {
+ const promises = [];
+ const sampling = 5;
+ for (let i = 0; i < sampling; i++) {
+ promises.push(BLApi.getProducts(i, keyword, filter));
+ }
+
+ // set loading
+ dispatch(fetching(true));
+
+ return axios.all(promises).then(responses => {
+ dispatch(fetching(false));
+ dispatch(calculating(true));
+
+ // tell ui component that fetching is completed
+ const tmp_resp = responses.map(r => r.data);
+ console.log("Return dari server adalah ");
+ console.log(tmp_resp);
+ // collecting all datas
+ let prices = [];
+ tmp_resp.forEach(t => {
+ prices = prices.concat(BLApi.parsePrice(t));
+ });
+
+ const result = BLApi.mathAnalysis(prices);
+
+ // tell ui component that calculation is completed
+ dispatch(calculating(false));
+ dispatch(setData(result));
+ })
+ .catch(error => {
+ throw (error);
+ });
+ };
+}
+
+// ================================================
+// Local functions, not exported
+// ================================================
diff --git a/js-src/redux/pricing/constant.js b/js-src/redux/pricing/constant.js
new file mode 100644
index 0000000..8c28813
--- /dev/null
+++ b/js-src/redux/pricing/constant.js
@@ -0,0 +1,4 @@
+export const PRICING_ACTION_SET = 'PRICING_ACTION_SET';
+export const PRICING_ACTION_FILTER = 'PRICING_ACTION_FILTER';
+export const PRICING_ACTION_FETCHING = 'PRICING_ACTION_LOADING';
+export const PRICING_ACTION_CALCULATING = 'PRICING_ACTION_CALCULATING';
diff --git a/js-src/redux/pricing/reducer.js b/js-src/redux/pricing/reducer.js
new file mode 100644
index 0000000..5912228
--- /dev/null
+++ b/js-src/redux/pricing/reducer.js
@@ -0,0 +1,34 @@
+import {
+ PRICING_ACTION_SET,
+ PRICING_ACTION_FILTER,
+ PRICING_ACTION_FETCHING,
+ PRICING_ACTION_CALCULATING,
+} from './constant';
+
+const initialState = {
+ graph: [], // array of object, please refer to react-node-pathjs-charts format
+ max_price: 0,
+ min_price: 0,
+ avg_price: 0,
+ best_price: 0,
+ filter: {}, // Please refer to superagent passing params to http get. used to store filter
+ isFetching: false,
+ isCalculating: false,
+};
+
+export default function userReducer(state = initialState, action) {
+ switch (action.type) {
+ case PRICING_ACTION_SET : {
+ const { min_price, max_price, avg_price, best_price, graph } = action.data;
+ return { ...state, max_price, min_price, avg_price, best_price, graph };
+ }
+ case PRICING_ACTION_FILTER :
+ return state;
+ case PRICING_ACTION_FETCHING :
+ return { ...state, isFetching: action.flag };
+ case PRICING_ACTION_CALCULATING :
+ return { ...state, isCalculating: action.flag };
+ default:
+ return state;
+ }
+}
diff --git a/js-src/redux/pricing_filter/actions.js b/js-src/redux/pricing_filter/actions.js
new file mode 100644
index 0000000..74d8141
--- /dev/null
+++ b/js-src/redux/pricing_filter/actions.js
@@ -0,0 +1,25 @@
+import {
+ PRICINGFILTER_ACTION_SET,
+ PRICINGFILTER_ACTION_CLEAR,
+} from './constant';
+// ini apa bib ? ntar tanyain ya .. pas hari h
+// User action types
+
+// User action creator
+export function setData(key, value) {
+ return {
+ type: PRICINGFILTER_ACTION_SET,
+ key,
+ value,
+ };
+}
+
+export function resetData() {
+ return {
+ type: PRICINGFILTER_ACTION_CLEAR,
+ };
+}
+
+// ================================================
+// Local functions, not exported
+// ================================================
diff --git a/js-src/redux/pricing_filter/constant.js b/js-src/redux/pricing_filter/constant.js
new file mode 100644
index 0000000..2740de6
--- /dev/null
+++ b/js-src/redux/pricing_filter/constant.js
@@ -0,0 +1,2 @@
+export const PRICINGFILTER_ACTION_SET = 'PRICINGFILTER_ACTION_SET';
+export const PRICINGFILTER_ACTION_CLEAR = 'PRICINGFILTER_ACTION_CLEAR';
diff --git a/js-src/redux/pricing_filter/reducer.js b/js-src/redux/pricing_filter/reducer.js
new file mode 100644
index 0000000..c230581
--- /dev/null
+++ b/js-src/redux/pricing_filter/reducer.js
@@ -0,0 +1,27 @@
+import {
+ PRICINGFILTER_ACTION_SET,
+ PRICINGFILTER_ACTION_CLEAR,
+} from './constant';
+
+const initialState = {
+ category_id: 159,
+ nego: 1, // harus dimapping..
+ harga_pas: 1, // harus dimapping
+ top_seller: 0,
+ conditions: '', // harus dimapping antara new / used sementara disabled
+ price_min: 0,
+ price_max: 99999999,
+ province: 'DKI Jakarta',
+ city: 'Jakarta Pusat',
+};
+
+export default function PricingFilterReducer(state = initialState, action) {
+ switch (action.type) {
+ case PRICINGFILTER_ACTION_SET :
+ return { ...state, [action.key]: action.value };
+ case PRICINGFILTER_ACTION_CLEAR :
+ return { ...state, ...initialState };
+ default:
+ return state;
+ }
+}
diff --git a/js-src/redux/user/actions.js b/js-src/redux/user/actions.js
index a4983cc..1474c2d 100644
--- a/js-src/redux/user/actions.js
+++ b/js-src/redux/user/actions.js
@@ -1,6 +1,46 @@
+import BLApi from '@lib/BLApi'
+import { Sqlite } from '@lib/BLSqlite';
+
// User action types
-export const USER = [{
- LOGIN: 'USER_LOGIN'
-}]
+export const USER = {
+ LOADING: 'USER_LOADING',
+ LOGIN_SUCCESS: 'USER_LOGIN_SUCCESS',
+ LOGIN_FAILED: 'USER_LOGIN_FAILED'
+}
+
+// User action creator
+export function login(username, password) {
+ return function(dispatch) {
+ dispatch(isLoading())
+
+ let cred = { username, password }
+ return BLApi.authenticateUser(cred, data => dispatch(loginSuccess(data)), err => dispatch(loginFailed()))
+ }
+}
+
+function isLoading() {
+ return {
+ type: USER.LOADING
+ }
+}
+
+function loginSuccess(data) {
+ Sqlite.insertToken({
+ userId: data.user_id,
+ token: data.token
+ })
+
+ return {
+ type: USER.LOGIN_SUCCESS,
+ payload: {
+ userId: data.user_id,
+ token: data.token
+ }
+ }
+}
-// User action creator
\ No newline at end of file
+function loginFailed() {
+ return {
+ type: USER.LOGIN_FAILED
+ }
+}
\ No newline at end of file
diff --git a/js-src/redux/user/reducer.js b/js-src/redux/user/reducer.js
index 45d7ab1..7b7c895 100644
--- a/js-src/redux/user/reducer.js
+++ b/js-src/redux/user/reducer.js
@@ -1,7 +1,32 @@
-const initialState = {}
+import { USER } from '@redux/user/actions'
+
+const initialState = {
+ loading: false,
+ userId: 0,
+ token: ''
+}
export default function userReducer(state = initialState, action) {
switch(action.type) {
+ case USER.LOADING:
+ return {
+ ...state,
+ loading: true
+ }
+ case USER.LOGIN_SUCCESS:
+ return {
+ ...state,
+ loading: false,
+ userId: action.payload.userId,
+ token: action.payload.token
+ }
+ case USER.LOGIN_FAILED:
+ return {
+ ...state,
+ loading: false,
+ userId: 0,
+ token: ''
+ }
default:
return state
}
diff --git a/js-src/screen/BidAnalysis/BidAnalysisContainer.js b/js-src/screen/BidAnalysis/BidAnalysisContainer.js
index 6cd0403..818c1d0 100644
--- a/js-src/screen/BidAnalysis/BidAnalysisContainer.js
+++ b/js-src/screen/BidAnalysis/BidAnalysisContainer.js
@@ -1,13 +1,19 @@
import { connect } from 'react-redux';
// Actions
+import { refreshBiddingData } from '@redux/bidding/actions';
+import { refreshData } from '@redux/dashboard/actions'
-import BidAnalysis from './BidAnalysisView'
+import BidAnalysis from './BidAnalysisView';
const mapStateToProps = (state) => ({
+ bidding: state.bidding,
+ dashboard: state.dashboard,
});
const mapDispatchToProps = {
+ refreshBiddingData: refreshBiddingData,
+ refreshData: refreshData,
};
-export default connect(mapStateToProps, mapDispatchToProps)(BidAnalysis)
\ No newline at end of file
+export default connect(mapStateToProps, mapDispatchToProps)(BidAnalysis);
diff --git a/js-src/screen/BidAnalysis/BidAnalysisView.js b/js-src/screen/BidAnalysis/BidAnalysisView.js
index 5088cdd..3201625 100644
--- a/js-src/screen/BidAnalysis/BidAnalysisView.js
+++ b/js-src/screen/BidAnalysis/BidAnalysisView.js
@@ -1,10 +1,13 @@
import React, { Component } from 'react'
import { StyleSheet, ScrollView, View, TouchableOpacity, Dimensions } from 'react-native';
import SubHeader from '@ui/SubHeader';
-import { AppStyles } from '@theme/';
+import { AppColors, AppSizes, AppStyles } from '@theme/'
import { Alerts, Button, Card, Spacer, Text } from '@components/ui/';
import Icon from 'react-native-vector-icons/MaterialIcons'
import Table from 'react-native-simple-table';
+import truncate from 'truncate';
+import numeral from 'numeral';
+import moment from 'moment';
const { width, height } = Dimensions.get("window");
const half_width = width/2;
@@ -17,7 +20,7 @@ const columns = [
},
{
title: 'Bid',
- dataIndex: 'bid',
+ dataIndex: 'bid_suggestion',
width: 70,
},
];
@@ -119,34 +122,33 @@ const dataSource = [
class BidAnalysis extends Component{
- render() {
- return (
-
-
-
+ getDateRange = () => {
+ let { latestDate } = this.props.dashboard
+ let momentObj = moment(latestDate, 'X')
-
-
-
-
- 22 April - 29 April
- 14 April - 21 April
-
+ let sundayDateStr = momentObj.format('D MMM')
+ let mondayDateStr = momentObj.day(-6).format('D MMM')
-
-
-
-
-
-
-
-
-
-
+ let range = mondayDateStr + ' - ' + sundayDateStr
-
-
+ return range
+ }
+
+ render() {
+ const data_bid_suggestion = this.props.bidding.bid_suggestion.map(val => {
+ let _bid_suggestion = val.bid_suggestion; // pembulatan
+ const _item = truncate(val.item, 25);
+ if(val.bid_suggestion >= 10000) {
+ _bid_suggestion = 10000;
+ }
+ return {
+ item: _item,
+ bid_suggestion: numeral(_bid_suggestion).format('0,0'),
+ };
+ });
+ return (
+
@@ -154,11 +156,10 @@ class BidAnalysis extends Component{
- Title of post
-
- seharusnya ini diagram , nanti aing lagi cari librarynya
-
-
+
+
+
@@ -201,6 +202,17 @@ const styles = StyleSheet.create({
marginTop: 10,
padding:5,
},
+ buttonWrapper: {
+ marginBottom: 10,
+ },
+ dateHeaderContainer: {
+ ...AppStyles.spreadHorizontalContainer,
+ ...AppStyles.paddingHorizontal,
+ backgroundColor: AppColors.brand.lightPrimary,
+ elevation: 4,
+ height: 56,
+ alignItems: 'center',
+ },
});
export default BidAnalysis
diff --git a/js-src/screen/Home/HomeContainer.js b/js-src/screen/Home/HomeContainer.js
index dfd9a7c..2a5abc9 100644
--- a/js-src/screen/Home/HomeContainer.js
+++ b/js-src/screen/Home/HomeContainer.js
@@ -1,14 +1,16 @@
import { connect } from 'react-redux';
// Actions
-import * as UserActions from '@redux/user/actions';
+import { refreshData } from '@redux/dashboard/actions'
import Home from './HomeView'
const mapStateToProps = (state) => ({
+ dashboard: state.dashboard
});
const mapDispatchToProps = {
+ refreshData
};
-export default connect(mapStateToProps, mapDispatchToProps)(Home)
\ No newline at end of file
+export default connect(mapStateToProps, mapDispatchToProps)(Home)
diff --git a/js-src/screen/Home/HomeView.js b/js-src/screen/Home/HomeView.js
index 4ef94be..bdea074 100644
--- a/js-src/screen/Home/HomeView.js
+++ b/js-src/screen/Home/HomeView.js
@@ -1,7 +1,12 @@
import React, { Component } from 'react'
-import { Dimensions, ScrollView, Text, TouchableOpacity, View } from 'react-native'
+import { ActivityIndicator, Dimensions, ScrollView, Text, TouchableOpacity, View } from 'react-native'
import { AppColors, AppSizes, AppStyles } from '@theme/'
import Icon from 'react-native-vector-icons/MaterialIcons'
+import { Pie } from 'react-native-pathjs-charts'
+import Table from 'react-native-simple-table';
+import moment from 'moment'
+import numeral from 'numeral'
+import truncate from 'truncate'
const { width: screenWidth, height: screenHeight } = Dimensions.get('window')
const viewStatWidth = (screenWidth-40)/7 // -40 karena paddingHorizontal
@@ -11,6 +16,42 @@ class Home extends Component {
super(props)
}
+ componentDidMount() {
+ this.props.refreshData()
+ }
+
+ getDateRange = () => {
+ let { latestDate } = this.props.dashboard
+ let momentObj = moment(latestDate, 'X')
+
+ let sundayDateStr = momentObj.format('D MMM')
+ let mondayDateStr = momentObj.day(-6).format('D MMM')
+
+ let range = mondayDateStr + ' - ' + sundayDateStr
+
+ return range
+ }
+
+ getPreviousDateRange = () => {
+ let { latestDate } = this.props.dashboard
+ let momentObj = moment(latestDate, 'X')
+
+ let sundayDateStr = momentObj.day(-7).format('D MMM')
+ let mondayDateStr = momentObj.day(-6).format('D MMM')
+
+ let range = mondayDateStr + ' - ' + sundayDateStr
+
+ return range
+ }
+
+ getRevenue= () => {
+ let { currentRevenue, fetchingTransaction } = this.props.dashboard
+
+ if (fetchingTransaction) return
+
+ return { numeral(currentRevenue).format('0.0 a') }
+ }
+
renderViewStat = (item, index) => {
return (
@@ -19,68 +60,262 @@ class Home extends Component {
)
}
+ renderRevenueComparation = () => {
+ let { currentRevenue, previousRevenue } = this.props.dashboard
+ let _style = {};
+ if(currentRevenue < (0.8 * previousRevenue)){
+ _style = styles.statusTagBad;
+ } else if (currentRevenue < (1.2 * previousRevenue)) { // implicitly imply that the revenue above 0.8 since it fails in above code
+ _style = styles.statusTagWarning;
+ } else {
+ _style = styles.statusTagOk;
+ }
+ return (
+ Compared to Prev Period
+ );
+ }
+
+ renderConvertionRateComparation = () => {
+ const { convertion_rate, prev_convertion_rate } = this.props.dashboard;
+ const curr_sold_accumulated = convertion_rate.sold_accumulated || 0;
+ const curr_view_accumulated = convertion_rate.view_accumulated || 1;
+ const prev_sold_accumulated = prev_convertion_rate.sold_accumulated || 0;
+ const prev_view_accumulated = prev_convertion_rate.view_accumulated || 1;
+ const conv_rate = curr_sold_accumulated / curr_view_accumulated * 100;
+ const prev_conv_rate = prev_sold_accumulated / prev_view_accumulated * 100;
+ console.log("conv rate", conv_rate, " prev conv_rate", prev_conv_rate);
+ let _style = {};
+ if(conv_rate < (0.8 * prev_conv_rate)){
+ _style = styles.statusTagBad;
+ } else if (conv_rate < (1.2 * prev_conv_rate)) { // implicitly imply that the revenue above 0.8 since it fails in above code
+ _style = styles.statusTagWarning;
+ } else {
+ _style = styles.statusTagOk;
+ }
+ return (
+ Compared to Prev Period
+ );
+ }
+
+ retrieveWeeklyData() {
+ const { weekly_view } = this.props.dashboard;
+ const view_stat = [];
+ view_stat[0] = weekly_view.Monday || 0;
+ view_stat[1] = weekly_view.Tuesday || 0;
+ view_stat[2] = weekly_view.Wednesday || 0;
+ view_stat[3] = weekly_view.Thursday || 0;
+ view_stat[4] = weekly_view.Friday || 0;
+ view_stat[5] = weekly_view.Saturday || 0;
+ view_stat[6] = weekly_view.Sunday || 0;
+ return view_stat;
+ }
+
render() {
+ console.log("state are: ", this.props.dashboard);
+ // Retriveing data from sqlite, truncating, and formating the number if needed
+ viewStat = this.retrieveWeeklyData();
+ const { least_viewed, most_viewed, convertion_rate, revenue_attribution, prev_convertion_rate } = this.props.dashboard;
+ const dataSource_least_viewed = least_viewed.map( d => {
+ return {
+ item: truncate(d.item, 25),
+ view: numeral(d.view).format('0.0 a'),
+ };
+ });
+ const dataSource_most_viewed = most_viewed.map( d => {
+ return {
+ item: truncate(d.item, 25),
+ view: numeral(d.view).format('0.0 a'),
+ };
+ });
+ console.log(revenue_attribution);
+ const data_revenue = revenue_attribution.map( d => {
+ return {
+ name: truncate(d.name, 25),
+ attribution: d.attribution,
+ };
+ });
+ const conv_rate = numeral(convertion_rate.sold_accumulated / convertion_rate.view_accumulated * 100).format('0.0');
+ const prev_conv_rate = numeral(prev_convertion_rate.sold_accumulated / prev_convertion_rate.view_accumulated * 100).format('0.0');
+
+
let maxViewStat = Math.max(...viewStat)
- let viewStatDiameters = viewStat.map((item) => item*80/maxViewStat)
+ let viewStatDiameters = viewStat.map((item) => item*viewStatWidth/maxViewStat)
return (
-
+
{/* Date Header */}
- 22 April - 29 April
- 14 April - 21 April
+ { this.getDateRange() }
+ { this.getPreviousDateRange() }
-
+ this.props.refreshData('prev')}
+ >
-
+ this.props.refreshData('next')}
+ >
{/* End of Date Header */}
+
+ Overview
+
+
+ Revenue
+ { this.getRevenue() }
+ { this.renderRevenueComparation() }
+
+
+ Conv. Rate
+ {conv_rate} %
+ { this.renderConvertionRateComparation()}
+
+
- Overview
-
-
- Revenue
- 400 K
- Compared to Prev Period
+ User Views by Day
+
+
+ Senin
+ Selasa
+ Rabu
+ Kamis
+ Jumat
+ Sabtu
+ Minggu
+
+
+ { viewStatDiameters.map(this.renderViewStat) }
+
-
- Conv. Rate
- 3 %
- Compared to Prev Period
+
+ Revenue Attribution
+
+
-
- User Views by Day
-
-
- Senin
- Selasa
- Rabu
- Kamis
- Jumat
- Sabtu
- Minggu
+ Most Viewed Product
+
+
-
- { viewStatDiameters.map(this.renderViewStat) }
+
+ Least Viewed Product
+
+
-
-
+
+
)
}
}
// To be replaced by dynamic data
const viewStat = [12, 15, 20, 8, 22, 30, 45]
+const data = [{
+ "name": "Washington",
+ "attribution": 7694980
+ }, {
+ "name": "Oregon",
+ "attribution": 2584160
+ }, {
+ "name": "Minnesota",
+ "attribution": 6590667,
+ "color": {'r':223,'g':154,'b':20}
+ }, {
+ "name": "Alaska",
+ "attribution": 7284698
+ }]
+const options = {
+ margin: {
+ top: 20,
+ left: 20,
+ right: 20,
+ bottom: 20
+ },
+ width: 350,
+ height: 350,
+ color: '#2980B9',
+ r: 50,
+ R: 150,
+ legendPosition: 'topLeft',
+ animate: {
+ type: 'oneByOne',
+ duration: 200,
+ fillTransition: 3
+ },
+ label: {
+ fontFamily: 'Arial',
+ fontSize: 8,
+ fontWeight: true,
+ color: '#ECF0F1'
+ }
+ }
+const columns = [
+ {
+ title: 'Item',
+ dataIndex: 'item',
+ width: AppSizes.widthHalf-40,
+ },
+ {
+ title: 'View',
+ dataIndex: 'view',
+ width: AppSizes.widthHalf-40,
+ },
+ // {
+ // title: 'Avg. Market View',
+ // dataIndex: 'avg',
+ // width: AppSizes.widthThird-40,
+ // }
+];
+const dataSource = [{
+ item: 'Item A',
+ view: 500,
+ avg: 450
+}, {
+ item: 'Item B',
+ view: 400,
+ avg: 394
+}, {
+ item: 'Item C',
+ view: 300,
+ avg: 145
+}, {
+ item: 'Item D',
+ view: 120,
+ avg: 254
+}]
const styles = {
centeredH3: {
@@ -89,12 +324,15 @@ const styles = {
},
centeredStat: {
...AppStyles.textCenterAligned,
- fontSize: 56
+ fontSize: 48
},
dateHeaderContainer: {
...AppStyles.spreadHorizontalContainer,
- ...AppStyles.padding,
- backgroundColor: AppColors.brand.lightPrimary
+ ...AppStyles.paddingHorizontal,
+ backgroundColor: AppColors.brand.lightPrimary,
+ elevation: 4,
+ height: 56,
+ alignItems: 'center',
},
overviewContainer: {
...AppStyles.row,
@@ -102,25 +340,41 @@ const styles = {
...AppStyles.paddingVertical,
backgroundColor: AppColors.background
},
+ revenueContainer: {
+ ...AppStyles.row,
+ ...AppStyles.paddingVertical,
+ backgroundColor: AppColors.background
+ },
sectionTitle: {
- ...AppStyles.h1,
+ ...AppStyles.h2,
...AppStyles.padding
},
statContainer: {
- ...AppStyles.flex1,
- ...AppStyles.paddingHorizontal
+ ...AppStyles.flex1
},
statusTagOk: {
...AppStyles.textCenterAligned,
backgroundColor: AppColors.brand.success,
padding: 4,
- borderRadius: 16
+ borderRadius: 16,
+ marginHorizontal: 4,
+ fontSize: 10
+ },
+ statusTagBad: {
+ ...AppStyles.textCenterAligned,
+ backgroundColor: AppColors.brand.lightPrimary,
+ padding: 4,
+ borderRadius: 16,
+ marginHorizontal: 4,
+ fontSize: 10
},
statusTagWarning: {
...AppStyles.textCenterAligned,
backgroundColor: AppColors.brand.accent,
padding: 4,
- borderRadius: 16
+ borderRadius: 16,
+ marginHorizontal: 4,
+ fontSize: 10
},
viewStatSectionContainer: {
...AppStyles.paddingHorizontal,
@@ -134,8 +388,9 @@ const styles = {
},
viewStatDay: {
...AppStyles.textCenterAligned,
- width: viewStatWidth // -40 karena paddingHorizontal
+ width: viewStatWidth,
+ fontSize: 10
}
}
-export default Home
\ No newline at end of file
+export default Home
diff --git a/js-src/screen/Login/LoginContainer.js b/js-src/screen/Login/LoginContainer.js
index 5595dc0..22a7837 100644
--- a/js-src/screen/Login/LoginContainer.js
+++ b/js-src/screen/Login/LoginContainer.js
@@ -1,13 +1,16 @@
import { connect } from 'react-redux';
+import { login } from '@redux/user/actions'
// Actions
import Login from './LoginView'
const mapStateToProps = (state) => ({
+ user: state.user
});
const mapDispatchToProps = {
+ login
};
export default connect(mapStateToProps, mapDispatchToProps)(Login)
\ No newline at end of file
diff --git a/js-src/screen/Login/LoginView.js b/js-src/screen/Login/LoginView.js
index 64bd5f7..c3819cf 100644
--- a/js-src/screen/Login/LoginView.js
+++ b/js-src/screen/Login/LoginView.js
@@ -1,20 +1,44 @@
import React, { Component } from 'react'
-import { Button, Text, View } from 'react-native'
+import { Button, Text, ToastAndroid, View, ScrollView } from 'react-native'
+import { FormLabel, FormInput } from '@components/ui'
+import { AppColors } from '@theme/'
class Login extends Component{
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ username: '',
+ password: ''
+ }
+ }
+
render() {
return (
-
- Login Screen
+
+
+ Username
+ this.setState({username})}
+ />
+ Password
+ this.setState({password})}
+ />
+
+
)
}
}
diff --git a/js-src/screen/PricingAnalysis/PricingAnalysisContainer.js b/js-src/screen/PricingAnalysis/PricingAnalysisContainer.js
index de15523..12f58a1 100644
--- a/js-src/screen/PricingAnalysis/PricingAnalysisContainer.js
+++ b/js-src/screen/PricingAnalysis/PricingAnalysisContainer.js
@@ -1,13 +1,19 @@
import { connect } from 'react-redux';
// Actions
+import * as PricingActions from '@redux/pricing/actions';
-import PricingAnalysis from './PricingAnalysisView'
+import PricingAnalysis from './PricingAnalysisView';
-const mapStateToProps = (state) => ({
-});
+const mapStateToProps = (state) => {
+ const { min_price, max_price, avg_price, best_price, graph, isFetching, isCalculating } = state.pricing;
+ const pricing_filter = state.pricing_filter;
+ return { max_price, min_price, avg_price, best_price, graph, pricing_filter, isFetching, isCalculating };
+};
const mapDispatchToProps = {
+ getGraph: PricingActions.getGraph,
+ setFilter: PricingActions.filter,
};
-export default connect(mapStateToProps, mapDispatchToProps)(PricingAnalysis)
\ No newline at end of file
+export default connect(mapStateToProps, mapDispatchToProps)(PricingAnalysis);
diff --git a/js-src/screen/PricingAnalysis/PricingAnalysisView.js b/js-src/screen/PricingAnalysis/PricingAnalysisView.js
index 078d7ae..ccc6f62 100644
--- a/js-src/screen/PricingAnalysis/PricingAnalysisView.js
+++ b/js-src/screen/PricingAnalysis/PricingAnalysisView.js
@@ -1,11 +1,12 @@
import React, { Component } from 'react';
-import { StyleSheet, ScrollView, View, TouchableOpacity } from 'react-native';
+import { ActivityIndicator, StyleSheet, ScrollView, View, TouchableOpacity } from 'react-native';
import SubHeader from '@ui/SubHeader';
import { AppStyles } from '@theme/';
import { Alerts, Button, Card, Spacer, Text } from '@components/ui/';
import FormWrapper from 'tcomb-form-native';
import _ from 'lodash';
import Icon from 'react-native-vector-icons/FontAwesome'
+import numeral from 'numeral';
import { Bar } from 'react-native-pathjs-charts';
@@ -23,12 +24,8 @@ const FORM_FIELDS = FormWrapper.struct({
search: FormWrapper.String,
});
-const FORM_OPTIONS = {
- auto: 'placeholders',
- stylesheet: FormWrapperStyle,
-};
-
class PricingAnalysis extends Component{
+
static navigationOptions = {
title: 'Example',
};
@@ -37,43 +34,119 @@ class PricingAnalysis extends Component{
super(props)
}
- render() {
- const Form = FormWrapper.form.Form;
+ FORM_OPTIONS = {
+ auto: 'placeholders',
+ stylesheet: FormWrapperStyle,
+ fields: {
+ search: {
+ onSubmitEditing: this.submitForm.bind(this),
+ }
+ }
+ }
+
+ submitForm(){
+ const form = this.form.getValue();
+ const pricing_filter = this.props.pricing_filter;
+ this.props.getGraph(form.search, pricing_filter);
+ }
+ renderPriceAnalysis() {
+ if (this.props.isFetching) {
+ return
+ } else {
+ return(
+
+
+
+ Terendah
+ {numeral(this.props.min_price).format('0,0.0 a')}
+
+
+ Rata-Rata
+ {numeral(this.props.avg_price).format('0,0.0 a')}
+
+
+ Termahal
+ {numeral(this.props.max_price).format('0,0.0 a')}
+
+
+
+
+
+
+ Terbaik
+ {numeral(this.props.best_price).format('0,0.0 a')}
+
+
+
+
+ );
+ }
+ }
+
+ renderbar(){
let data = [
[{
- "v": 49,
- "name": "apple"
- }, {
- "v": 42,
- "name": "apple"
+ "v": 1,
+ "name": "1"
+ }],
+ [{
+ "v": 1,
+ "name": "2"
+ }],
+ [{
+ "v": 1,
+ "name": "3"
+ }],
+ [{
+ "v": 1,
+ "name": "4"
+ }],
+ [{
+ "v": 1,
+ "name": "5"
+ }],
+ [{
+ "v": 1,
+ "name": "6"
+ }],
+ [{
+ "v": 1,
+ "name": "7"
}],
[{
- "v": 69,
- "name": "banana"
- }, {
- "v": 62,
- "name": "banana"
+ "v": 1,
+ "name": "8"
}],
[{
- "v": 29,
- "name": "grape"
- }, {
- "v": 15,
- "name": "grape"
+ "v": 1,
+ "name": "9"
+ }],
+ [{
+ "v": 1,
+ "name": "10"
}]
];
+ const graphdata = this.props.graph;
+ if (graphdata.length > 0) {
+ let test_data = [];
+ graphdata.forEach(g => {
+ test_data.push([g]);
+ });
+ data = test_data;
+ }
+
let options = {
- width: 300,
- height: 300,
+ width: 220,
+ height: 220,
margin: {
top: 20,
left: 25,
bottom: 50,
right: 20
},
- color: '#2980B9',
+ color: '#D71149',
gutter: 20,
animate: {
type: 'oneByOne',
@@ -83,7 +156,7 @@ class PricingAnalysis extends Component{
axisX: {
showAxis: true,
showLines: true,
- showLabels: true,
+ showLabels: false,
showTicks: true,
zeroAxis: false,
orient: 'bottom',
@@ -110,19 +183,41 @@ class PricingAnalysis extends Component{
}
};
+ if(this.props.isFetching){
+ return (
+
+ Retrieving data from Bukalapak Server, Running our magic algorithm
+
+
+ );
+ }
+ if(this.props.graph.length > 0 ) {
+ return
+ }else{
+ return Start searching a keyword
+ }
+ }
+
+ render() {
+ const Form = FormWrapper.form.Form;
+ const Bar = this.renderbar();
return (
@@ -138,41 +233,15 @@ class PricingAnalysis extends Component{
onPress={()=>{ console.log('somethng'); }}
>
-
+ { Bar }
- Title of post
-
- seharusnya ini diagram , nanti aing lagi cari librarynya
-
-
-
- Terendah
- 10K
-
-
- Rata-Rata
- 20K
-
-
- Termahal
- 30K
-
-
-
-
-
-
- Terbaik
- 20K
-
-
-
+ {this.renderPriceAnalysis()}
@@ -183,7 +252,6 @@ class PricingAnalysis extends Component{
}
}
-
const styles = StyleSheet.create({
container: {
flex: 1,
diff --git a/js-src/screen/PricingFilter/PricingFilterContainer.js b/js-src/screen/PricingFilter/PricingFilterContainer.js
new file mode 100644
index 0000000..973cb65
--- /dev/null
+++ b/js-src/screen/PricingFilter/PricingFilterContainer.js
@@ -0,0 +1,63 @@
+import { connect } from 'react-redux';
+
+// Actions
+import * as PricingFilterActions from '@redux/pricing_filter/actions';
+
+// View
+import PricingFilter from './PricingFilterView'
+
+const mapStateToProps = (state) => {
+ // preprocess dulu baru mapping manual..
+ const {
+ category_id,
+ nego,
+ harga_pas,
+ top_seller,
+ conditions,
+ price_min,
+ price_max,
+ province,
+ city,
+ } = state.pricing_filter;
+
+ let bisa_nego = '';
+ if (nego === 1 ){
+ if (harga_pas === 1) {
+ bisa_nego = 'all_prices';
+ } else {
+ bisa_nego = 'nego_only';
+ }
+ } else {
+ bisa_nego = 'fixed_only';
+ }
+
+ let _top_seller = false;
+ if(top_seller == 1) {
+ _top_seller = true;
+ }
+
+ let _province = '';
+ if(province) {
+ _province = province.replace(" ","_");
+ }
+
+ const retval = {
+ form_value: {
+ kategori: `_${category_id}`,
+ bisa_nego: bisa_nego,
+ top_seller: _top_seller,
+ province: _province,
+ city: city,
+ price_min: price_min,
+ price_max: price_max,
+ },
+ }
+ return retval;
+};
+
+const mapDispatchToProps = {
+ setData: PricingFilterActions.setData,
+ resetData: PricingFilterActions.resetData
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(PricingFilter)
diff --git a/js-src/screen/PricingFilter/PricingFilterView.js b/js-src/screen/PricingFilter/PricingFilterView.js
new file mode 100644
index 0000000..af9d551
--- /dev/null
+++ b/js-src/screen/PricingFilter/PricingFilterView.js
@@ -0,0 +1,292 @@
+import React, { Component } from 'react';
+import { StyleSheet, View, Button, ScrollView, TouchableOpacity } from 'react-native';
+import Icon from 'react-native-vector-icons/Entypo';
+import { Text } from '@ui';
+import AutoInput from '@ui/AutoInput';
+import { AppStyles, AppSizes } from '@theme';
+import FormWrapper from 'tcomb-form-native';
+import _ from 'lodash';
+
+// === Configuring form ===
+const Form = FormWrapper.form.Form;
+const styling = _.cloneDeep(FormWrapper.form.Form.stylesheet);
+
+//-- styling
+styling.formGroup.normal.flexDirection = 'row';
+styling.formGroup.error.flexDirection = 'row';
+styling.formGroup.normal.alignItems = 'center';
+styling.formGroup.error.alignItems = 'center';
+
+styling.textbox.normal.width = AppSizes.screen.widthTwoThirds;
+styling.textbox.error.width = AppSizes.screen.widthTwoThirds;
+styling.textbox.normal.backgroundColor = '#fff';
+styling.textbox.error.backgroundColor = '#fff';
+
+styling.controlLabel.normal.width = AppSizes.screen.widthThird;
+styling.controlLabel.error.width = AppSizes.screen.widthThird;
+
+styling.select.normal.width = AppSizes.screen.widthTwoThirds;
+styling.select.error.width = AppSizes.screen.widthTwoThirds;
+styling.select.normal.height = 36;
+styling.select.error.height = 36;
+styling.select.normal.borderRadius = 4;
+styling.select.error.borderRadius = 4;
+styling.select.normal.backgroundColor = '#fff';
+styling.select.error.backgroundColor = '#fff';
+
+styling.checkbox.normal.height = 36;
+styling.checkbox.error.height = 36;
+
+styling.select.normal.borderRadius = 4;
+styling.select.error.borderRadius = 4;
+
+const FORM_CATEGORIES = FormWrapper.enums({
+ _2266: 'Perawatan & Kecantikan',
+ _2359: 'Kesehatan',
+ _159: 'Fashion Wanita',
+ _164: 'Fashion Pria',
+ _7: 'Handphone',
+ _1: 'Komputer',
+ _510: 'Elektronik',
+ _10: 'Kamera',
+ _58: 'Hobi & Koleksi',
+ _61: 'Olahraga',
+ _64: 'Sepeda',
+ _13: 'Fashion Anak',
+ _68: 'Perlengkapan Bayi',
+ _65: 'Rumah Tangga',
+ _139: 'Food',
+ _19: 'Mobil Part & Accessories',
+ _471: 'Motor',
+ _1648: 'Industrial',
+ _70: 'Perlengkapan Kantor',
+ _1695: 'Tiket & Voucher',
+});
+
+const FORM_PROVINCES = FormWrapper.enums({
+ Bali: 'Bali',
+ Banten: 'Banten',
+ Bengkulu: 'Bengkulu',
+ Daerah_Istimewa_Yogyakarta: 'Daerah Istimewa Yogyakarta',
+ DKI_Jakarta: 'DKI Jakarta',
+ Gorontalo: 'Gorontalo',
+ Jambi: 'Jambi',
+ Jawa_Barat: 'Jawa Barat',
+ Jawa_Tengah: 'Jawa Tengah',
+ Jawa_Timur: 'Jawa Timur',
+ Kalimantan_Barat: 'Kalimantan Barat',
+ Kalimantan_Selatan: 'Kalimantan Selatan',
+ Kalimantan_Tengah: 'Kalimantan Tengah',
+ Kalimantan_Timur: 'Kalimantan Timur',
+ Kalimantan_Utara: 'Kalimantan Utara',
+ Kepulauan_Bangka_Belitung: 'Kepulauan Bangka Belitung',
+ Kepulauan_Riau: 'Kepulauan Riau',
+ Lampung: 'Lampung',
+ Maluku: 'Maluku',
+ Maluku_Utara: 'Maluku Utara',
+ Nanggroe_Aceh_Darussalam: 'Nanggroe Aceh Darussalam',
+ Nusa_Tenggara_Barat: 'Nusa Tenggara Barat',
+ Nusa_Tenggara_Timur: 'Nusa Tenggara Timur',
+ Papua: 'Papua',
+ Papua_Barat: 'Papua Barat',
+ Riau: 'Riau',
+ Sulawesi_Barat: 'Sulawesi Barat',
+ Sulawesi_Selatan: 'Sulawesi Selatan',
+ Sulawesi_Tengah: 'Sulawesi Tengah',
+ Sulawesi_Tenggara: 'Sulawesi Tenggara',
+ Sulawesi_Utara: 'Sulawesi Utara',
+ Sumatera_Barat: 'Sumatera Barat',
+ Sumatera_Selatan: 'Sumatera Selatan',
+ Sumatera_Utara: 'Sumatera Utara',
+});
+
+const FORM_CONDITIONS = FormWrapper.enums({
+ all_conditions: 'semua barang',
+ new_conditions: 'baru',
+ second_conditions: 'bekas',
+});
+
+const FORM_NEGOTIABLE = FormWrapper.enums({
+ all_prices: 'semua jenis',
+ nego_only: 'nego',
+ fixed_only: 'harga pas',
+});
+
+const FORM_PRICING = FormWrapper.struct({
+ kategori: FormWrapper.maybe(FORM_CATEGORIES),
+ province: FormWrapper.maybe(FORM_PROVINCES),
+ city: FormWrapper.maybe(FormWrapper.String),
+ bisa_nego: FormWrapper.maybe(FORM_NEGOTIABLE),
+ top_seller: FormWrapper.maybe(FormWrapper.Boolean),
+ condtions: FormWrapper.maybe(FORM_CONDITIONS), // conditions
+ price_min: FormWrapper.maybe(FormWrapper.Number),
+ price_max: FormWrapper.maybe(FormWrapper.Number),
+});
+
+// === Main Component ===
+class PricingFilter extends Component {
+ navigation = null;
+
+
+ static navigationOptions = {
+ title: 'Advanced Filter',
+ headerLeft: null,
+ headerRight: (
+ navigation.navigate('PricingAnalysis')}
+ style={{justifyContent: 'center', alignItems: 'center', padding: 8, paddingTop: 12}}>
+
+
+ ),
+ };
+
+ constructor(props) {
+ super(props);
+ navigation = this.props.navigation;
+ }
+
+ handleSubmit() {
+ const value = this.form.getValue();
+ console.log("getvalue di handlesubmit",value);
+ if (value) {
+ this.props.setData('category_id', parseInt(value.kategori.replace("_","")));
+
+ if (value.bisa_nego == 'all_prices') {
+ this.props.setData('nego', 1);
+ this.props.setData('harga_pas', 1);
+ } else if (value.bisa_nego == 'nego_only') {
+ this.props.setData('nego', 1);
+ this.props.setData('harga_pas', 0);
+ } else if (value.bisa_nego == 'fixed_only') {
+ this.props.setData('nego', 0);
+ this.props.setData('harga_pas', 1);
+ }
+
+ if (value.top_seller) {
+ this.props.setData('top_seller', 1);
+ } else {
+ this.props.setData('top_seller', 0);
+ }
+
+ if (value.conditions == 'all_conditions') {
+ this.props.setData('conditions', 'new');
+ } else if (value.conditions == 'new_conditions') {
+ this.props.setData('conditions', 'new');
+ } else if (value.conditions == 'second_conditions') {
+ this.props.setData('conditions', 'used');
+ }
+
+ this.props.setData('price_min', value.price_min);
+ this.props.setData('price_max', value.price_max);
+
+ if (value.province && value.city) {
+ this.props.setData('province', value.province.replace("_"," "));
+ this.props.setData('city', value.city);
+ }
+
+ }
+ }
+
+ render() {
+ let FORM_VALUE = {
+ category_id: "_159",
+ city: "Jakarta",
+ };
+
+ if (this.props.form_value ) {
+ FORM_VALUE = {
+ category_id: this.props.form_value.category_id,
+ };
+ }
+
+ const FORM_OPTIONS = {
+ stylesheet: styling,
+ i18n: {
+ optional: '',
+ },
+ fields: {
+ city: {
+ factory: AutoInput,
+ config: {
+ elements: ['Badung', 'Bangli', 'Buleleng', 'Denpasar', 'Gianyar', 'Jembrana', 'Karangasem', 'Klungkung', 'Tabanan', 'Cilegon', 'Lebak', 'Pandeglang', 'Serang', 'Kab. Serang', 'Tangerang', 'Kab. Tangerang', 'Tangerang Selatan', 'Bengkulu', 'Bengkulu Selatan', 'Bengkulu Tengah', 'Bengkulu Utara', 'Kaur', 'Kepahiang', 'Lebong', 'Mukomuko', 'Rejang Lebong', 'Seluma', 'Bantul', 'Gunung Kidul', 'Kulon Progo', 'Sleman', 'Yogyakarta', 'Jakarta Barat', 'Jakarta Pusat', 'Jakarta Selatan', 'Jakarta Timur', 'Jakarta Utara', 'Kepulauan Seribu', 'Boalemo', 'Bone Bolango', 'Gorontalo', 'Kab. Gorontalo', 'Gorontalo Utara', 'Pohuwato', 'Batang Hari', 'Bungo', 'Jambi', 'Kerinci', 'Merangin', 'Muaro Jambi', 'Sarolangun', 'Sungai Penuh', 'Tanjung Jabung Barat', 'Tanjung Jabung Timur', 'Tebo', 'Bandung', 'Kab. Bandung', 'Bandung Barat', 'Banjar', 'Bekasi', 'Kab. Bekasi', 'Bogor', 'Kab. Bogor', 'Ciamis', 'Cianjur', 'Cimahi', 'Cirebon', 'Kab. Cirebon', 'Depok', 'Garut', 'Indramayu', 'Karawang', 'Kuningan', 'Majalengka', 'Pangandaran', 'Purwakarta', 'Subang', 'Sukabumi', 'Kab. Sukabumi', 'Sumedang', 'Tasikmalaya', 'Kab. Tasikmalaya', 'Banjarnegara', 'Banyumas', 'Batang', 'Blora', 'Boyolali', 'Brebes', 'Cilacap', 'Demak', 'Grobogan', 'Jepara', 'Karanganyar', 'Kebumen', 'Kendal', 'Klaten', 'Kudus', 'Magelang', 'Kab. Magelang', 'Pati', 'Pekalongan', 'Kab. Pekalongan', 'Pemalang', 'Purbalingga', 'Purworejo', 'Rembang', 'Salatiga', 'Semarang', 'Kab. Semarang', 'Sragen', 'Solo', 'Sukoharjo', 'Tegal', 'Kab. Tegal', 'Temanggung', 'Wonogiri', 'Wonosobo', 'Bangkalan', 'Banyuwangi', 'Batu', 'Blitar', 'Kab. Blitar', 'Bojonegoro', 'Bondowoso', 'Gresik', 'Jember', 'Jombang', 'Kediri', 'Kab. Kediri', 'Lamongan', 'Lumajang', 'Madiun', 'Kab. Madiun', 'Magetan', 'Malang', 'Kab. Malang', 'Mojokerto', 'Kab. Mojokerto', 'Nganjuk', 'Ngawi', 'Pacitan', 'Pamekasan', 'Pasuruan', 'Kab. Pasuruan', 'Ponorogo', 'Probolinggo', 'Kab. Probolinggo', 'Sampang', 'Sidoarjo', 'Situbondo', 'Sumenep', 'Surabaya', 'Trenggalek', 'Tuban', 'Tulungagung', 'Bengkayang', 'Kapuas Hulu', 'Kayong Utara', 'Ketapang', 'Kubu Raya', 'Landak', 'Melawi', 'Pontianak', 'Kab. Pontianak', 'Sambas', 'Sanggau', 'Sekadau', 'Singkawang', 'Sintang', 'Balangan', 'Kab. Banjar', 'Banjarbaru', 'Banjarmasin', 'Barito Kuala', 'Hulu Sungai Selatan', 'Hulu Sungai Tengah', 'Hulu Sungai Utara', 'Kotabaru', 'Tabalong', 'Tanah Bumbu', 'Tanah Laut', 'Tapin', 'Barito Selatan', 'Barito Timur', 'Barito Utara', 'Gunung Mas', 'Kapuas', 'Katingan', 'Kotawaringin Barat', 'Kotawaringin Timur', 'Lamandau', 'Murung Raya', 'Palangkaraya', 'Pulang Pisau', 'Seruyan', 'Sukamara', 'Balikpapan', 'Berau', 'Bontang', 'Kutai Barat', 'Kutai Kartanegara', 'Kutai Timur', 'Mahakam Ulu', 'Paser', 'Penajam Paser Utara', 'Samarinda', 'Bulungan', 'Malinau', 'Nunukan', 'Tana Tidung', 'Tarakan', 'Bangka', 'Bangka Barat', 'Bangka Selatan', 'Bangka Tengah', 'Belitung', 'Belitung Timur', 'Pangkal Pinang', 'Batam', 'Bintan', 'Karimun', 'Kepulauan Anambas', 'Lingga', 'Natuna', 'Tanjung Pinang', 'Bandar Lampung', 'Lampung Barat', 'Lampung Selatan', 'Lampung Tengah', 'Lampung Timur', 'Lampung Utara', 'Mesuji', 'Metro', 'Pesawaran', 'Pesisir Barat', 'Pringsewu', 'Tanggamus', 'Tulang Bawang', 'Tulang Bawang Barat', 'Way Kanan', 'Ambon', 'Buru', 'Buru Selatan', 'Kepulauan Aru', 'Maluku Barat Daya', 'Maluku Tengah', 'Maluku Tenggara', 'Maluku Tenggara Barat', 'Seram Bagian Barat', 'Seram Bagian Timur', 'Seram Barat', 'Seram Timur', 'Tual', 'Halmahera Barat', 'Halmahera Selatan', 'Halmahera Tengah', 'Halmahera Timur', 'Halmahera Utara', 'Kepulauan Sula', 'Pulau Morotai', 'Ternate', 'Tidore Kepulauan', 'Aceh Barat', 'Aceh Barat Daya', 'Aceh Besar', 'Aceh Jaya', 'Aceh Selatan', 'Aceh Singkil', 'Aceh Tamiang', 'Aceh Tengah', 'Aceh Tenggara', 'Aceh Timur', 'Aceh Utara', 'Banda Aceh', 'Bener Meriah', 'Bireuen', 'Gayo Lues', 'Langsa', 'Lhokseumawe', 'Nagan Raya', 'Pidie', 'Pidie Jaya', 'Sabang', 'Simeulue', 'Subulussalam', 'Bima', 'Kab. Bima', 'Dompu', 'Lombok Barat', 'Lombok Tengah', 'Lombok Timur', 'Lombok Utara', 'Mataram', 'Sumbawa', 'Sumbawa Barat', 'Alor', 'Belu', 'Ende', 'Flores Timur', 'Kupang', 'Kab. Kupang', 'Lembata', 'Malaka', 'Manggarai', 'Manggarai Barat', 'Manggarai Timur', 'Nagekeo', 'Ngada', 'Rote Ndao', 'Sabu Raijua', 'Sikka', 'Sumba Barat', 'Sumba Barat Daya', 'Sumba Tengah', 'Sumba Timur', 'Timor Tengah Selatan', 'Timor Tengah Utara', 'Asmat', 'Biak Numfor', 'Boven Digoel', 'Deiyai', 'Dogiyai', 'Intan Jaya', 'Jayapura', 'Kab. Jayapura', 'Jayawijaya', 'Keerom', 'Kepulauan Yapen', 'Lanny Jaya', 'Mamberamo Raya', 'Mamberamo Tengah', 'Mappi', 'Merauke', 'Mimika', 'Nabire', 'Nduga', 'Paniai', 'Pegunungan Bintang', 'Puncak', 'Puncak Jaya', 'Sarmi', 'Supiori', 'Tolikara', 'Waropen', 'Yahukimo', 'Yalimo', 'Fakfak', 'Kaimana', 'Manokwari', 'Manokwari Selatan', 'Maybrat', 'Pegunungan Arfak', 'Raja Ampat', 'Sorong', 'Kab. Sorong', 'Sorong Selatan', 'Tambrauw', 'Teluk Bintuni', 'Teluk Wondama', 'Bengkalis', 'Dumai', 'Indragiri Hilir', 'Indragiri Hulu', 'Kampar', 'Kepulauan Meranti', 'Kuantan Singingi', 'Pekanbaru', 'Pelalawan', 'Rokan Hilir', 'Rokan Hulu', 'Siak', 'Majene', 'Mamasa', 'Mamuju', 'Mamuju Tengah', 'Mamuju Utara', 'Polewali Mandar', 'Bantaeng', 'Barru', 'Bone', 'Bulukumba', 'Enrekang', 'Gowa', 'Jeneponto', 'Kepulauan Selayar', 'Luwu', 'Luwu Timur', 'Luwu Utara', 'Makassar', 'Maros', 'Palopo', 'Pangkajene dan Kepulauan', 'Parepare', 'Pinrang', 'Sidenreng Rappang', 'Sinjai', 'Soppeng', 'Takalar', 'Tana Toraja', 'Toraja Utara', 'Wajo', 'Banggai', 'Banggai Kepulauan', 'Banggai Laut', 'Buol', 'Donggala', 'Morowali', 'Morowali Utara', 'Palu', 'Parigi Moutong', 'Poso', 'Sigi', 'Tojo Una-Una', 'Toli-Toli', 'Bau-Bau', 'Bombana', 'Buton', 'Buton Selatan', 'Buton Tengah', 'Buton Utara', 'Kendari', 'Kolaka', 'Kolaka Timur', 'Kolaka Utara', 'Konawe', 'Konawe Kepulauan', 'Konawe Selatan', 'Konawe Utara', 'Muna', 'Wakatobi', 'Bitung', 'Bolaang Mongondow', 'Bolaang Mongondow Selatan', 'Bolaang Mongondow Timur', 'Bolaang Mongondow Utara', 'Kepulauan Sangihe', 'Kepulauan Siau Tagulandang Biaro', 'Kepulauan Talaud', 'Kotamobagu', 'Manado', 'Minahasa', 'Minahasa Selatan', 'Minahasa Tenggara', 'Minahasa Utara', 'Tomohon', 'Agam', 'Bukittinggi', 'Dharmasraya', 'Kepulauan Mentawai', 'Lima Puluh Kota', 'Padang', 'Padang Pariaman', 'Padangpanjang', 'Pariaman', 'Pasaman', 'Pasaman Barat', 'Payakumbuh', 'Pesisir Selatan', 'Sawahlunto', 'Sijunjung', 'Solok', 'Kab. Solok', 'Solok Selatan', 'Tanah Datar', 'Banyuasin', 'Empat Lawang', 'Lahat', 'Lubuklinggau', 'Muara Enim', 'Musi Banyuasin', 'Musi Rawas', 'Ogan Ilir', 'Ogan Komering Ilir', 'Ogan Komering Ulu', 'Ogan Komering Ulu Selatan', 'Ogan Komering Ulu Timur', 'Pagar Alam', 'Palembang', 'Penukal Abab Lematang Ilir', 'Prabumulih', 'Asahan', 'Batu Bara', 'Binjai', 'Dairi', 'Deli Serdang', 'Gunung Sitoli', 'Humbang Hasundutan', 'Karo', 'Labuhanbatu', 'Labuhanbatu Selatan', 'Labuhanbatu Utara', 'Langkat', 'Mandailing Natal', 'Medan', 'Nias', 'Nias Barat', 'Nias Selatan', 'Nias Utara', 'Padang Lawas', 'Padang Lawas Utara', 'Padang Sidempuan', 'Pakpak Barat', 'Pematangsiantar', 'Samosir', 'Serdang Bedagai', 'Sibolga', 'Simalungun', 'Tanjung Balai', 'Tapanuli Selatan', 'Tapanuli Tengah', 'Tapanuli Utara', 'Tebing Tinggi', 'Toba Samosir'],
+ propForQuery: 'name',
+ },
+ label: 'City',
+ },
+ category_id: {
+ selectedValue: '_159'
+ }
+ },
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ },
+ hello: {
+ fontSize: 20,
+ textAlign: 'center',
+ margin: 10,
+ },
+ scrollview: {
+ flex: 1
+ },
+ formArea: {
+ flex: 9,
+ paddingTop: 15,
+ },
+ submitArea: {
+ flex: 1,
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ alignItems: 'center',
+ backgroundColor: '#fff',
+ },
+});
+
+export default PricingFilter;
diff --git a/package.json b/package.json
index 6daf75f..5d5a7be 100644
--- a/package.json
+++ b/package.json
@@ -18,9 +18,13 @@
},
"homepage": "https://github.com/bukaanalytics/app#readme",
"dependencies": {
+ "axios": "^0.16.1",
"lodash": "^4.17.4",
+ "moment": "^2.18.1",
+ "numeral": "^2.0.6",
"react": "15.4.1",
"react-native": "^0.42.0",
+ "react-native-autocomplete-input": "^3.2.1",
"react-native-elements": "^0.12.1",
"react-native-pathjs-charts": "0.0.26",
"react-native-side-menu": "^0.20.1",
@@ -33,8 +37,11 @@
"react-redux": "^5.0.4",
"redux": "^3.6.0",
"redux-logger": "^3.0.1",
+ "redux-persist": "^4.8.0",
"redux-thunk": "^2.2.0",
- "tcomb-form-native": "^0.6.8"
+ "superagent": "^3.5.2",
+ "tcomb-form-native": "^0.6.8",
+ "truncate": "^2.0.0"
},
"jest": {
"preset": "react-native"