cindyy287 commited on
Commit
c2fb337
·
verified ·
1 Parent(s): fad8ac0

Upload 23 files

Browse files
app/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask
2
+ from flask_cors import CORS
3
+ from .routes import api
4
+
5
+ # Create and configure the Flask application exported by this package
6
+ app = Flask(__name__)
7
+ CORS(app)
8
+
9
+ app.register_blueprint(api)
app/model.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import joblib
2
+ import json
3
+ import os
4
+ import numpy as np
5
+ import pandas as pd
6
+
7
+ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
8
+ MODEL_DIR = os.path.join(BASE_DIR, "models")
9
+ def _load_first_existing(*names):
10
+ """Try the given filenames in order and load the first one that exists.
11
+ Returns the loaded object or raises FileNotFoundError if none exist.
12
+ """
13
+ for name in names:
14
+ path = os.path.join(MODEL_DIR, name)
15
+ if os.path.exists(path):
16
+ return joblib.load(path)
17
+ raise FileNotFoundError(f"None of {names} found in {MODEL_DIR}")
18
+
19
+
20
+ # Load model and preprocessor, preferring enhanced versions if present.
21
+ model = _load_first_existing(
22
+ "ensemble_model_enhanced.joblib",
23
+ "ensemble_model.joblib",
24
+ "Ensemble_model.joblib",
25
+ )
26
+
27
+ preprocessor = _load_first_existing(
28
+ "preprocessor_enhanced.joblib",
29
+ "preprocessor.joblib",
30
+ "Preprocessor.joblib",
31
+ )
32
+
33
+ # Anscombe config (case-insensitive check)
34
+ anscombe_path = None
35
+ for candidate in ("anscombe.json", "Anscombe.json"):
36
+ p = os.path.join(MODEL_DIR, candidate)
37
+ if os.path.exists(p):
38
+ anscombe_path = p
39
+ break
40
+ if anscombe_path:
41
+ with open(anscombe_path) as f:
42
+ anscombe_config = json.load(f)
43
+ else:
44
+ anscombe_config = {}
45
+
46
+
47
+ def predict_fraud(data: dict):
48
+ # Accept either a dict of feature-name: value pairs or a JSON
49
+ # body with a single key "features" containing a list of values.
50
+ if isinstance(data, dict) and "features" in data:
51
+ features = data["features"]
52
+ # If the preprocessor expects named columns, provide a
53
+ # DataFrame with those column names; otherwise use a numpy
54
+ # array truncated/padded to the expected length.
55
+ feature_names = getattr(preprocessor, "feature_names_in_", None)
56
+ if feature_names is not None:
57
+ cols = list(feature_names)
58
+ row = features[: len(cols)]
59
+ # Figure out which columns are treated as categorical by the
60
+ # preprocessor so we can coerce values appropriately.
61
+ cat_cols = set()
62
+ for name, trans, cols_in_transformer in preprocessor.transformers_:
63
+ try:
64
+ # If transformer is OneHotEncoder (or similar) we
65
+ # treat its columns as categorical.
66
+ if type(trans).__name__ == "OneHotEncoder" or hasattr(trans, 'categories_'):
67
+ for c in cols_in_transformer:
68
+ cat_cols.add(c)
69
+ except Exception:
70
+ continue
71
+
72
+ coerced = []
73
+ for col_name, v in zip(cols, row):
74
+ if col_name in cat_cols:
75
+ coerced.append(str(v))
76
+ else:
77
+ try:
78
+ coerced.append(float(v))
79
+ except Exception:
80
+ coerced.append(float('nan'))
81
+ # If the provided features list is shorter than the number
82
+ # of expected columns, pad the remaining columns with
83
+ # sensible defaults: empty string for categorical columns
84
+ # and NaN for numeric columns.
85
+ if len(row) < len(cols):
86
+ for col_name in cols[len(row) :]:
87
+ if col_name in cat_cols:
88
+ coerced.append("")
89
+ else:
90
+ coerced.append(float('nan'))
91
+ X = pd.DataFrame([coerced], columns=cols)
92
+ else:
93
+ X = np.array([features])
94
+ else:
95
+ # If caller provided a mapping of name->value, use a
96
+ # DataFrame so column names match the preprocessor.
97
+ if isinstance(data, dict):
98
+ X = pd.DataFrame([data])
99
+ else:
100
+ X = np.array([list(data.values())])
101
+ # Ensure the input has the expected number of features for the
102
+ # preprocessor. If extra features are provided (e.g. tests send 4
103
+ # but preprocessor expects 2), take the first n features.
104
+ expected = getattr(preprocessor, "n_features_in_", None)
105
+ if expected is not None:
106
+ # If X is a numpy array, check shape; if it's a DataFrame,
107
+ # the preprocessor can accept it as long as it has required
108
+ # columns.
109
+ if isinstance(X, np.ndarray):
110
+ if X.shape[1] < expected:
111
+ raise ValueError(f"X has {X.shape[1]} features, but preprocessor is expecting {expected} features as input.")
112
+ if X.shape[1] > expected:
113
+ X = X[:, :expected]
114
+
115
+ try:
116
+ X_processed = preprocessor.transform(X)
117
+ except Exception as exc:
118
+ # Raise a more informative error to help debugging
119
+ cols = getattr(X, 'columns', None)
120
+ head = None
121
+ try:
122
+ head = X.head().to_dict()
123
+ except Exception:
124
+ head = None
125
+ raise ValueError(f"Transform failed: {exc}; X_type={type(X)}; columns={cols}; head={head}") from exc
126
+
127
+ prediction = model.predict(X_processed)[0]
128
+ probability = model.predict_proba(X_processed)[0].max()
129
+
130
+ return {
131
+ "fraud": int(prediction),
132
+ "fraud_prediction": int(prediction),
133
+ "probability": float(probability)
134
+ }
135
+
136
+
app/routes.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify
2
+ from app.model import predict_fraud
3
+
4
+ api = Blueprint("api", __name__)
5
+
6
+
7
+ @api.route("/predict", methods=["POST"])
8
+ def predict():
9
+ try:
10
+ data = request.get_json()
11
+
12
+ if not data:
13
+ return jsonify({"error": "No input data"}), 400
14
+
15
+ result = predict_fraud(data)
16
+
17
+ return jsonify(result), 200
18
+
19
+ except Exception as e:
20
+ return jsonify({"error": str(e)}), 500
app/schemas.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # dokumentasi aja, belum dipakai
2
+ PredictRequest = {
3
+ "features": list
4
+ }
5
+
6
+ PredictResponse = {
7
+ "fraud": bool
8
+ }
features/__init__.py ADDED
File without changes
features/feature_builder.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ from datetime import datetime
4
+
5
+ # DAFTAR KOLOM SESUAI ERROR TERAKHIR (WAJIB LENGKAP)
6
+ FEATURE_COLUMNS = [
7
+ "location",
8
+ "amount",
9
+ "customer_lat",
10
+ "customer_long",
11
+ "merchant_lat",
12
+ "merchant_long",
13
+ "distance_customer_merchant",
14
+ "customer_city_population",
15
+ "customer_no_transactions",
16
+ "customer_no_orders",
17
+ "customer_no_payments",
18
+ "payments_per_order_ratio",
19
+ "transactions_per_customer_ratio",
20
+ "age",
21
+ "customer_gender",
22
+ "customer_job",
23
+ "customer_place_name",
24
+ "customer_zip_code",
25
+ "merchant_id",
26
+ "merchant_name",
27
+ "transaction_type",
28
+ "transaction_category",
29
+ "hour_of_day",
30
+ "day_of_week",
31
+ "fraud_rate_by_location",
32
+ "mean_amount_by_location",
33
+ "avg_amount_per_transaction",
34
+ "amount_per_city_pop",
35
+ "amount_deviation_from_location_mean"
36
+ ]
37
+
38
+ def build_features(input_data: dict) -> pd.DataFrame:
39
+ """
40
+ Build 1-row DataFrame with all features required by the model.
41
+ Missing features are filled with safe defaults.
42
+ """
43
+
44
+ now = datetime.now()
45
+
46
+ feature_dict = {
47
+ "location": input_data.get("location", 0),
48
+ "amount": input_data.get("amount", 0),
49
+
50
+ # customer
51
+ "customer_lat": 0.0,
52
+ "customer_long": 0.0,
53
+ "customer_city_population": 0,
54
+ "customer_no_transactions": 0,
55
+ "customer_no_orders": 0,
56
+ "customer_no_payments": 0,
57
+ "payments_per_order_ratio": 0.0,
58
+ "transactions_per_customer_ratio": 0.0,
59
+ "age": 0,
60
+ "customer_gender": np.nan,
61
+ "customer_job": np.nan,
62
+ "customer_place_name": np.nan,
63
+ "customer_zip_code": np.nan,
64
+
65
+ # merchant
66
+ "merchant_id": np.nan,
67
+ "merchant_name": np.nan,
68
+ "merchant_lat": 0.0,
69
+ "merchant_long": 0.0,
70
+
71
+ # transaction
72
+ "transaction_type": np.nan,
73
+ "transaction_category": np.nan,
74
+
75
+ # time
76
+ "hour_of_day": now.hour,
77
+ "day_of_week": now.weekday(),
78
+
79
+ # engineered / aggregate
80
+ "distance_customer_merchant": 0.0,
81
+ "fraud_rate_by_location": 0.0,
82
+ "mean_amount_by_location": 0.0,
83
+ "avg_amount_per_transaction": 0.0,
84
+ "amount_per_city_pop": 0.0,
85
+ "amount_deviation_from_location_mean": 0.0,
86
+ }
87
+
88
+ df = pd.DataFrame([feature_dict])
89
+
90
+ # PASTIKAN URUTAN KOLOM SESUAI TRAINING
91
+ df = df[FEATURE_COLUMNS]
92
+
93
+ return df
frontend/fraud_detection_frontend.html ADDED
@@ -0,0 +1,2196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Fraud Detection System</title>
7
+ <!-- PWA: manifest & theme -->
8
+ <link rel="manifest" href="/manifest.json">
9
+ <meta name="theme-color" content="#3498db">
10
+ <link rel="icon" href="/icons/icon.svg" sizes="192x192">
11
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
12
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
13
+ <style>
14
+ :root {
15
+ --primary-color: #2c3e50;
16
+ --secondary-color: #3498db;
17
+ --success-color: #27ae60;
18
+ --danger-color: #e74c3c;
19
+ --warning-color: #f39c12;
20
+ --info-color: #17a2b8;
21
+ --light-bg: #f8f9fa;
22
+ --dark-text: #2c3e50;
23
+ --border-radius: 10px;
24
+ --box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
25
+ --transition: all 0.3s ease;
26
+ }
27
+
28
+ * {
29
+ margin: 0;
30
+ padding: 0;
31
+ box-sizing: border-box;
32
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
33
+ }
34
+
35
+ body {
36
+ background-color: #f5f7fa;
37
+ color: var(--dark-text);
38
+ line-height: 1.6;
39
+ }
40
+
41
+ .container {
42
+ max-width: 1400px;
43
+ margin: 0 auto;
44
+ padding: 20px;
45
+ }
46
+
47
+ /* Header Styles */
48
+ header {
49
+ background-color: white;
50
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
51
+ position: sticky;
52
+ top: 0;
53
+ z-index: 1000;
54
+ }
55
+
56
+ .header-content {
57
+ display: flex;
58
+ justify-content: space-between;
59
+ align-items: center;
60
+ padding: 15px 0;
61
+ }
62
+
63
+ .logo {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 15px;
67
+ }
68
+
69
+ .logo-icon {
70
+ background-color: var(--primary-color);
71
+ color: white;
72
+ width: 50px;
73
+ height: 50px;
74
+ border-radius: 50%;
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ font-size: 1.5rem;
79
+ }
80
+
81
+ .logo-text h1 {
82
+ font-size: 1.8rem;
83
+ color: var(--primary-color);
84
+ }
85
+
86
+ .logo-text p {
87
+ font-size: 0.9rem;
88
+ color: #666;
89
+ }
90
+
91
+ nav ul {
92
+ display: flex;
93
+ list-style: none;
94
+ gap: 25px;
95
+ }
96
+
97
+ nav a {
98
+ text-decoration: none;
99
+ color: var(--dark-text);
100
+ font-weight: 500;
101
+ padding: 8px 15px;
102
+ border-radius: var(--border-radius);
103
+ transition: var(--transition);
104
+ }
105
+
106
+ nav a:hover, nav a.active {
107
+ background-color: var(--primary-color);
108
+ color: white;
109
+ }
110
+
111
+ .user-section {
112
+ display: flex;
113
+ align-items: center;
114
+ gap: 20px;
115
+ }
116
+
117
+ .user-info {
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 10px;
121
+ }
122
+
123
+ .user-avatar {
124
+ width: 40px;
125
+ height: 40px;
126
+ background-color: var(--secondary-color);
127
+ border-radius: 50%;
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: center;
131
+ color: white;
132
+ font-weight: bold;
133
+ }
134
+
135
+ .logout-btn {
136
+ background-color: var(--danger-color);
137
+ color: white;
138
+ border: none;
139
+ padding: 8px 20px;
140
+ border-radius: var(--border-radius);
141
+ cursor: pointer;
142
+ font-weight: 500;
143
+ transition: var(--transition);
144
+ }
145
+
146
+ .logout-btn:hover {
147
+ background-color: #c0392b;
148
+ transform: translateY(-2px);
149
+ }
150
+
151
+ /* Main Content */
152
+ .main-content {
153
+ display: flex;
154
+ flex-direction: column;
155
+ gap: 30px;
156
+ margin-top: 30px;
157
+ }
158
+
159
+ /* Dashboard Cards */
160
+ .dashboard-cards {
161
+ display: grid;
162
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
163
+ gap: 25px;
164
+ }
165
+
166
+ .card {
167
+ background-color: white;
168
+ border-radius: var(--border-radius);
169
+ padding: 25px;
170
+ box-shadow: var(--box-shadow);
171
+ transition: var(--transition);
172
+ border-top: 4px solid transparent;
173
+ }
174
+
175
+ .card:hover {
176
+ transform: translateY(-5px);
177
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
178
+ }
179
+
180
+ .card.fraud {
181
+ border-top-color: var(--danger-color);
182
+ }
183
+
184
+ .card.safe {
185
+ border-top-color: var(--success-color);
186
+ }
187
+
188
+ .card.warning {
189
+ border-top-color: var(--warning-color);
190
+ }
191
+
192
+ .card.info {
193
+ border-top-color: var(--info-color);
194
+ }
195
+
196
+ .card-header {
197
+ display: flex;
198
+ justify-content: space-between;
199
+ align-items: center;
200
+ margin-bottom: 20px;
201
+ }
202
+
203
+ .card-icon {
204
+ width: 60px;
205
+ height: 60px;
206
+ border-radius: 50%;
207
+ display: flex;
208
+ align-items: center;
209
+ justify-content: center;
210
+ font-size: 1.8rem;
211
+ color: white;
212
+ }
213
+
214
+ .fraud .card-icon {
215
+ background-color: var(--danger-color);
216
+ }
217
+
218
+ .safe .card-icon {
219
+ background-color: var(--success-color);
220
+ }
221
+
222
+ .warning .card-icon {
223
+ background-color: var(--warning-color);
224
+ }
225
+
226
+ .info .card-icon {
227
+ background-color: var(--info-color);
228
+ }
229
+
230
+ .card-value {
231
+ font-size: 2.5rem;
232
+ font-weight: 700;
233
+ margin-bottom: 5px;
234
+ }
235
+
236
+ .card-label {
237
+ color: #666;
238
+ font-size: 1rem;
239
+ }
240
+
241
+ /* Section Header */
242
+ .section-header {
243
+ display: flex;
244
+ justify-content: space-between;
245
+ align-items: center;
246
+ margin-bottom: 25px;
247
+ }
248
+
249
+ .section-title {
250
+ font-size: 1.8rem;
251
+ color: var(--primary-color);
252
+ display: flex;
253
+ align-items: center;
254
+ gap: 15px;
255
+ }
256
+
257
+ .section-title i {
258
+ color: var(--secondary-color);
259
+ }
260
+
261
+ /* Prediction Form */
262
+ .prediction-form-container {
263
+ background-color: white;
264
+ border-radius: var(--border-radius);
265
+ padding: 30px;
266
+ box-shadow: var(--box-shadow);
267
+ }
268
+
269
+ .form-row {
270
+ display: grid;
271
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
272
+ gap: 25px;
273
+ margin-bottom: 25px;
274
+ }
275
+
276
+ .form-group {
277
+ margin-bottom: 20px;
278
+ }
279
+
280
+ .form-group label {
281
+ display: block;
282
+ margin-bottom: 8px;
283
+ font-weight: 600;
284
+ color: var(--primary-color);
285
+ }
286
+
287
+ .form-control {
288
+ width: 100%;
289
+ padding: 14px 20px;
290
+ border: 2px solid #e0e0e0;
291
+ border-radius: var(--border-radius);
292
+ font-size: 1rem;
293
+ transition: var(--transition);
294
+ background-color: #f9f9f9;
295
+ }
296
+
297
+ .form-control:focus {
298
+ outline: none;
299
+ border-color: var(--secondary-color);
300
+ background-color: white;
301
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
302
+ }
303
+
304
+ .form-note {
305
+ font-size: 0.9rem;
306
+ color: #666;
307
+ margin-top: 5px;
308
+ font-style: italic;
309
+ }
310
+
311
+ .btn {
312
+ padding: 14px 30px;
313
+ border: none;
314
+ border-radius: var(--border-radius);
315
+ cursor: pointer;
316
+ font-weight: 600;
317
+ font-size: 1rem;
318
+ transition: var(--transition);
319
+ display: inline-flex;
320
+ align-items: center;
321
+ gap: 10px;
322
+ }
323
+
324
+ .btn-primary {
325
+ background-color: var(--secondary-color);
326
+ color: white;
327
+ }
328
+
329
+ .btn-primary:hover {
330
+ background-color: #2980b9;
331
+ transform: translateY(-2px);
332
+ }
333
+
334
+ .btn-success {
335
+ background-color: var(--success-color);
336
+ color: white;
337
+ }
338
+
339
+ .btn-success:hover {
340
+ background-color: #219653;
341
+ }
342
+
343
+ .btn-danger {
344
+ background-color: var(--danger-color);
345
+ color: white;
346
+ }
347
+
348
+ .btn-danger:hover {
349
+ background-color: #c0392b;
350
+ }
351
+
352
+ .btn-block {
353
+ display: block;
354
+ width: 100%;
355
+ }
356
+
357
+ /* Results Section */
358
+ .results-container {
359
+ background-color: white;
360
+ border-radius: var(--border-radius);
361
+ padding: 30px;
362
+ box-shadow: var(--box-shadow);
363
+ margin-top: 20px;
364
+ display: none;
365
+ }
366
+
367
+ .results-header {
368
+ display: flex;
369
+ justify-content: space-between;
370
+ align-items: center;
371
+ margin-bottom: 25px;
372
+ padding-bottom: 15px;
373
+ border-bottom: 2px solid #f0f0f0;
374
+ }
375
+
376
+ .results-title {
377
+ font-size: 1.6rem;
378
+ color: var(--primary-color);
379
+ }
380
+
381
+ .prediction-badge {
382
+ padding: 10px 25px;
383
+ border-radius: 50px;
384
+ font-weight: 700;
385
+ font-size: 1.1rem;
386
+ text-transform: uppercase;
387
+ letter-spacing: 1px;
388
+ }
389
+
390
+ .badge-fraud {
391
+ background-color: rgba(231, 76, 60, 0.1);
392
+ color: var(--danger-color);
393
+ border: 2px solid var(--danger-color);
394
+ }
395
+
396
+ .badge-safe {
397
+ background-color: rgba(39, 174, 96, 0.1);
398
+ color: var(--success-color);
399
+ border: 2px solid var(--success-color);
400
+ }
401
+
402
+ .prediction-details {
403
+ display: grid;
404
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
405
+ gap: 25px;
406
+ margin-bottom: 30px;
407
+ }
408
+
409
+ .detail-box {
410
+ background-color: #f8f9fa;
411
+ padding: 20px;
412
+ border-radius: var(--border-radius);
413
+ border-left: 4px solid var(--secondary-color);
414
+ }
415
+
416
+ .detail-label {
417
+ font-size: 0.9rem;
418
+ color: #666;
419
+ margin-bottom: 5px;
420
+ }
421
+
422
+ .detail-value {
423
+ font-size: 1.3rem;
424
+ font-weight: 700;
425
+ color: var(--primary-color);
426
+ }
427
+
428
+ .probability-container {
429
+ margin: 30px 0;
430
+ }
431
+
432
+ .probability-header {
433
+ display: flex;
434
+ justify-content: space-between;
435
+ margin-bottom: 10px;
436
+ }
437
+
438
+ .probability-bar {
439
+ height: 25px;
440
+ background-color: #ecf0f1;
441
+ border-radius: 12px;
442
+ overflow: hidden;
443
+ margin-bottom: 15px;
444
+ }
445
+
446
+ .probability-fill {
447
+ height: 100%;
448
+ border-radius: 12px;
449
+ transition: width 1s ease;
450
+ }
451
+
452
+ .fraud-probability {
453
+ background: linear-gradient(90deg, #e74c3c, #c0392b);
454
+ }
455
+
456
+ .safe-probability {
457
+ background: linear-gradient(90deg, #27ae60, #219653);
458
+ }
459
+
460
+ .probability-text {
461
+ display: flex;
462
+ justify-content: space-between;
463
+ font-weight: 600;
464
+ color: var(--primary-color);
465
+ }
466
+
467
+ .feedback-section {
468
+ margin-top: 35px;
469
+ padding-top: 25px;
470
+ border-top: 2px solid #f0f0f0;
471
+ }
472
+
473
+ .feedback-title {
474
+ font-size: 1.2rem;
475
+ margin-bottom: 15px;
476
+ color: var(--primary-color);
477
+ }
478
+
479
+ .feedback-buttons {
480
+ display: flex;
481
+ gap: 15px;
482
+ }
483
+
484
+ .feedback-btn {
485
+ flex: 1;
486
+ padding: 15px;
487
+ border-radius: var(--border-radius);
488
+ background-color: #f8f9fa;
489
+ border: 2px solid #ddd;
490
+ cursor: pointer;
491
+ transition: var(--transition);
492
+ font-weight: 600;
493
+ display: flex;
494
+ flex-direction: column;
495
+ align-items: center;
496
+ gap: 8px;
497
+ }
498
+
499
+ .feedback-btn:hover {
500
+ background-color: #e9ecef;
501
+ }
502
+
503
+ .feedback-btn.active {
504
+ background-color: var(--secondary-color);
505
+ color: white;
506
+ border-color: var(--secondary-color);
507
+ }
508
+
509
+ /* Charts Section */
510
+ .charts-container {
511
+ display: grid;
512
+ grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
513
+ gap: 30px;
514
+ margin-bottom: 30px;
515
+ }
516
+
517
+ .chart-card {
518
+ background-color: white;
519
+ border-radius: var(--border-radius);
520
+ padding: 25px;
521
+ box-shadow: var(--box-shadow);
522
+ }
523
+
524
+ .chart-title {
525
+ font-size: 1.3rem;
526
+ margin-bottom: 20px;
527
+ color: var(--primary-color);
528
+ display: flex;
529
+ align-items: center;
530
+ gap: 10px;
531
+ }
532
+
533
+ .chart-wrapper {
534
+ position: relative;
535
+ height: 300px;
536
+ }
537
+
538
+ /* Transactions Table */
539
+ .transactions-container {
540
+ background-color: white;
541
+ border-radius: var(--border-radius);
542
+ padding: 30px;
543
+ box-shadow: var(--box-shadow);
544
+ margin-bottom: 30px;
545
+ }
546
+
547
+ .table-header {
548
+ display: flex;
549
+ justify-content: space-between;
550
+ align-items: center;
551
+ margin-bottom: 25px;
552
+ }
553
+
554
+ .table-actions {
555
+ display: flex;
556
+ gap: 15px;
557
+ }
558
+
559
+ .table-controls {
560
+ display: flex;
561
+ justify-content: space-between;
562
+ align-items: center;
563
+ margin-top: 20px;
564
+ padding-top: 20px;
565
+ border-top: 1px solid #eee;
566
+ }
567
+
568
+ table {
569
+ width: 100%;
570
+ border-collapse: collapse;
571
+ margin-top: 20px;
572
+ }
573
+
574
+ thead {
575
+ background-color: #f8f9fa;
576
+ }
577
+
578
+ th {
579
+ padding: 16px 20px;
580
+ text-align: left;
581
+ font-weight: 600;
582
+ color: var(--primary-color);
583
+ border-bottom: 2px solid #eee;
584
+ }
585
+
586
+ td {
587
+ padding: 16px 20px;
588
+ border-bottom: 1px solid #eee;
589
+ }
590
+
591
+ tr:hover {
592
+ background-color: #f9f9f9;
593
+ }
594
+
595
+ .status-badge {
596
+ padding: 6px 15px;
597
+ border-radius: 20px;
598
+ font-size: 0.85rem;
599
+ font-weight: 600;
600
+ text-align: center;
601
+ display: inline-block;
602
+ }
603
+
604
+ .status-fraud {
605
+ background-color: rgba(231, 76, 60, 0.1);
606
+ color: var(--danger-color);
607
+ border: 1px solid rgba(231, 76, 60, 0.3);
608
+ }
609
+
610
+ .status-safe {
611
+ background-color: rgba(39, 174, 96, 0.1);
612
+ color: var(--success-color);
613
+ border: 1px solid rgba(39, 174, 96, 0.3);
614
+ }
615
+
616
+ /* Feature Importance */
617
+ .feature-importance-container {
618
+ background-color: white;
619
+ border-radius: var(--border-radius);
620
+ padding: 30px;
621
+ box-shadow: var(--box-shadow);
622
+ margin-bottom: 30px;
623
+ }
624
+
625
+ .feature-list {
626
+ margin-top: 25px;
627
+ }
628
+
629
+ .feature-item {
630
+ display: flex;
631
+ align-items: center;
632
+ margin-bottom: 15px;
633
+ padding: 15px;
634
+ background-color: #f8f9fa;
635
+ border-radius: var(--border-radius);
636
+ border-left: 4px solid var(--secondary-color);
637
+ }
638
+
639
+ .feature-rank {
640
+ background-color: var(--primary-color);
641
+ color: white;
642
+ width: 35px;
643
+ height: 35px;
644
+ border-radius: 50%;
645
+ display: flex;
646
+ align-items: center;
647
+ justify-content: center;
648
+ font-weight: bold;
649
+ margin-right: 15px;
650
+ }
651
+
652
+ .feature-name {
653
+ flex-grow: 1;
654
+ font-weight: 600;
655
+ }
656
+
657
+ .feature-importance {
658
+ font-weight: 700;
659
+ color: var(--primary-color);
660
+ }
661
+
662
+ /* Notification System */
663
+ .notification-container {
664
+ position: fixed;
665
+ top: 20px;
666
+ right: 20px;
667
+ z-index: 2000;
668
+ max-width: 400px;
669
+ }
670
+
671
+ .notification {
672
+ background-color: white;
673
+ border-radius: var(--border-radius);
674
+ padding: 20px;
675
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
676
+ margin-bottom: 15px;
677
+ display: flex;
678
+ align-items: center;
679
+ gap: 15px;
680
+ transform: translateX(120%);
681
+ transition: transform 0.5s ease;
682
+ border-left: 5px solid var(--secondary-color);
683
+ }
684
+
685
+ .notification.show {
686
+ transform: translateX(0);
687
+ }
688
+
689
+ .notification.warning {
690
+ border-left-color: var(--warning-color);
691
+ }
692
+
693
+ .notification.danger {
694
+ border-left-color: var(--danger-color);
695
+ }
696
+
697
+ .notification.success {
698
+ border-left-color: var(--success-color);
699
+ }
700
+
701
+ .notification-icon {
702
+ font-size: 1.8rem;
703
+ }
704
+
705
+ .notification.warning .notification-icon {
706
+ color: var(--warning-color);
707
+ }
708
+
709
+ .notification.danger .notification-icon {
710
+ color: var(--danger-color);
711
+ }
712
+
713
+ .notification.success .notification-icon {
714
+ color: var(--success-color);
715
+ }
716
+
717
+ .notification-content h4 {
718
+ margin-bottom: 5px;
719
+ color: var(--primary-color);
720
+ }
721
+
722
+ .notification-content p {
723
+ color: #666;
724
+ font-size: 0.9rem;
725
+ }
726
+
727
+ /* Login Page */
728
+ .login-container {
729
+ display: flex;
730
+ justify-content: center;
731
+ align-items: center;
732
+ min-height: 80vh;
733
+ }
734
+
735
+ .login-card {
736
+ background-color: white;
737
+ border-radius: var(--border-radius);
738
+ padding: 40px;
739
+ box-shadow: var(--box-shadow);
740
+ width: 100%;
741
+ max-width: 450px;
742
+ }
743
+
744
+ .login-header {
745
+ text-align: center;
746
+ margin-bottom: 30px;
747
+ }
748
+
749
+ .login-header h2 {
750
+ color: var(--primary-color);
751
+ margin-bottom: 10px;
752
+ }
753
+
754
+ .login-header p {
755
+ color: #666;
756
+ }
757
+
758
+ /* Responsive Design */
759
+ @media (max-width: 1200px) {
760
+ .charts-container {
761
+ grid-template-columns: 1fr;
762
+ }
763
+ }
764
+
765
+ @media (max-width: 768px) {
766
+ .header-content {
767
+ flex-direction: column;
768
+ gap: 20px;
769
+ }
770
+
771
+ nav ul {
772
+ flex-wrap: wrap;
773
+ justify-content: center;
774
+ gap: 15px;
775
+ }
776
+
777
+ .form-row {
778
+ grid-template-columns: 1fr;
779
+ }
780
+
781
+ .dashboard-cards {
782
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
783
+ }
784
+
785
+ .charts-container {
786
+ grid-template-columns: 1fr;
787
+ }
788
+
789
+ .feedback-buttons {
790
+ flex-direction: column;
791
+ }
792
+
793
+ .table-header {
794
+ flex-direction: column;
795
+ align-items: flex-start;
796
+ gap: 15px;
797
+ }
798
+
799
+ .table-actions {
800
+ width: 100%;
801
+ justify-content: space-between;
802
+ }
803
+ }
804
+ </style>
805
+ </head>
806
+ <body>
807
+ <!-- Notification System -->
808
+ <div class="notification-container" id="notificationContainer"></div>
809
+
810
+ <!-- Header Section -->
811
+ <header>
812
+ <div class="container header-content">
813
+ <div class="logo">
814
+ <div class="logo-icon">
815
+ <i class="fas fa-shield-alt"></i>
816
+ </div>
817
+ <div class="logo-text">
818
+ <h1>Fraud Detection AI</h1>
819
+ <p>Real-time Transaction Monitoring System</p>
820
+ </div>
821
+ </div>
822
+
823
+ <nav>
824
+ <ul>
825
+ <li><a href="#" class="nav-link active" data-page="dashboard"><i class="fas fa-tachometer-alt"></i> Dashboard</a></li>
826
+ <li><a href="#" class="nav-link" data-page="predict"><i class="fas fa-search"></i> Fraud Prediction</a></li>
827
+ <li><a href="#" class="nav-link" data-page="transactions"><i class="fas fa-history"></i> Transactions</a></li>
828
+ <li><a href="#" class="nav-link" data-page="features"><i class="fas fa-chart-bar"></i> Feature Analysis</a></li>
829
+ </ul>
830
+ </nav>
831
+
832
+ <div class="user-section">
833
+ <div class="user-info">
834
+ <div class="user-avatar">AD</div>
835
+ <div>
836
+ <div class="user-name">Admin User</div>
837
+ <div class="user-role" style="font-size: 0.85rem; color: #666;">Administrator</div>
838
+ </div>
839
+ </div>
840
+ <!-- Backend URL control: paste your ngrok backend HTTPS URL here and click Set -->
841
+ <div style="display:flex; align-items:center; gap:8px; margin-right:12px;">
842
+ <input id="backendUrlInput" class="form-control" placeholder="Backend URL (https://...)" style="width:320px; padding:8px 12px; font-size:0.9rem;" />
843
+ <button id="setBackendUrlBtn" class="btn" style="background:#444; color:white; padding:8px 12px; border-radius:8px;">Set</button>
844
+ </div>
845
+ <button class="logout-btn" id="logoutBtn"><i class="fas fa-sign-out-alt"></i> Logout</button>
846
+ </div>
847
+ </div>
848
+ </header>
849
+
850
+ <!-- Main Content Container -->
851
+ <div class="container">
852
+ <!-- Dashboard Page -->
853
+ <div id="dashboard-page" class="page active">
854
+ <div class="main-content">
855
+ <!-- Dashboard Stats -->
856
+ <div class="dashboard-cards">
857
+ <div class="card fraud">
858
+ <div class="card-header">
859
+ <div>
860
+ <div class="card-value" id="totalFraud">0</div>
861
+ <div class="card-label">Fraudulent Transactions</div>
862
+ </div>
863
+ <div class="card-icon">
864
+ <i class="fas fa-exclamation-triangle"></i>
865
+ </div>
866
+ </div>
867
+ <div class="card-trend" style="color: var(--danger-color); font-weight: 600;">
868
+ <i class="fas fa-arrow-up"></i> 12% from last month
869
+ </div>
870
+ </div>
871
+
872
+ <div class="card safe">
873
+ <div class="card-header">
874
+ <div>
875
+ <div class="card-value" id="totalSafe">0</div>
876
+ <div class="card-label">Legitimate Transactions</div>
877
+ </div>
878
+ <div class="card-icon">
879
+ <i class="fas fa-check-circle"></i>
880
+ </div>
881
+ </div>
882
+ <div class="card-trend" style="color: var(--success-color); font-weight: 600;">
883
+ <i class="fas fa-arrow-up"></i> 8% from last month
884
+ </div>
885
+ </div>
886
+
887
+ <div class="card warning">
888
+ <div class="card-header">
889
+ <div>
890
+ <div class="card-value" id="totalTransactions">0</div>
891
+ <div class="card-label">Total Transactions</div>
892
+ </div>
893
+ <div class="card-icon">
894
+ <i class="fas fa-exchange-alt"></i>
895
+ </div>
896
+ </div>
897
+ <div class="card-trend" style="color: var(--warning-color); font-weight: 600;">
898
+ <i class="fas fa-arrow-up"></i> 15% from last month
899
+ </div>
900
+ </div>
901
+
902
+ <div class="card info">
903
+ <div class="card-header">
904
+ <div>
905
+ <div class="card-value" id="accuracyRate">0%</div>
906
+ <div class="card-label">Model Accuracy</div>
907
+ </div>
908
+ <div class="card-icon">
909
+ <i class="fas fa-brain"></i>
910
+ </div>
911
+ </div>
912
+ <div class="card-trend" style="color: var(--info-color); font-weight: 600;">
913
+ <i class="fas fa-arrow-up"></i> 3% improvement
914
+ </div>
915
+ </div>
916
+ </div>
917
+
918
+ <!-- Charts Section -->
919
+ <div class="section-header">
920
+ <h2 class="section-title"><i class="fas fa-chart-line"></i> Fraud Analytics Overview</h2>
921
+ </div>
922
+
923
+ <div class="charts-container">
924
+ <div class="chart-card">
925
+ <h3 class="chart-title"><i class="fas fa-chart-pie"></i> Fraud Distribution</h3>
926
+ <div class="chart-wrapper">
927
+ <canvas id="fraudDistributionChart"></canvas>
928
+ </div>
929
+ </div>
930
+
931
+ <div class="chart-card">
932
+ <h3 class="chart-title"><i class="fas fa-chart-bar"></i> Fraud by Location</h3>
933
+ <div class="chart-wrapper">
934
+ <canvas id="locationFraudChart"></canvas>
935
+ </div>
936
+ </div>
937
+ </div>
938
+
939
+ <!-- Recent Fraud Alerts -->
940
+ <div class="section-header">
941
+ <h2 class="section-title"><i class="fas fa-bell"></i> Recent Fraud Alerts</h2>
942
+ <button class="btn btn-primary" id="viewAllAlerts">
943
+ <i class="fas fa-eye"></i> View All
944
+ </button>
945
+ </div>
946
+
947
+ <div class="transactions-container">
948
+ <table id="recentAlertsTable">
949
+ <thead>
950
+ <tr>
951
+ <th>Transaction ID</th>
952
+ <th>Date & Time</th>
953
+ <th>Amount ($)</th>
954
+ <th>Location</th>
955
+ <th>Merchant</th>
956
+ <th>Status</th>
957
+ <th>Action</th>
958
+ </tr>
959
+ </thead>
960
+ <tbody id="recentAlertsBody">
961
+ <!-- Alerts will be loaded here -->
962
+ </tbody>
963
+ </table>
964
+ </div>
965
+ </div>
966
+ </div>
967
+
968
+ <!-- Fraud Prediction Page -->
969
+ <div id="predict-page" class="page">
970
+ <div class="section-header">
971
+ <h2 class="section-title"><i class="fas fa-search"></i> Fraud Detection Prediction</h2>
972
+ <div class="model-info" style="background-color: #e8f4fc; padding: 10px 20px; border-radius: var(--border-radius);">
973
+ <span style="font-weight: 600; color: var(--secondary-color);">
974
+ <i class="fas fa-robot"></i> Model: Ensemble (Random Forest + XGBoost)
975
+ </span>
976
+ </div>
977
+ </div>
978
+
979
+ <div class="prediction-form-container">
980
+ <form id="predictionForm">
981
+ <div class="form-row">
982
+ <div class="form-group">
983
+ <label for="transactionAmount">Transaction Amount ($)</label>
984
+ <input type="number" id="transactionAmount" class="form-control" placeholder="Enter transaction amount" step="0.01" min="0" required>
985
+ <div class="form-note">Based on dataset: Amount range from $4.30 to $4189.27</div>
986
+ </div>
987
+
988
+ <div class="form-group">
989
+ <label for="transactionLocation">Transaction Location</label>
990
+ <select id="transactionLocation" class="form-control" required>
991
+ <option value="">Select Location</option>
992
+ <option value="San Antonio">San Antonio</option>
993
+ <option value="Dallas">Dallas</option>
994
+ <option value="New York">New York</option>
995
+ <option value="Philadelphia">Philadelphia</option>
996
+ <option value="Phoenix">Phoenix</option>
997
+ <option value="Utah">Utah</option>
998
+ <option value="Maryland">Maryland</option>
999
+ <option value="New Mexico">New Mexico</option>
1000
+ <option value="South Dakota">South Dakota</option>
1001
+ <option value="Montana">Montana</option>
1002
+ <option value="Luar Negeri">International</option>
1003
+ </select>
1004
+ <div class="form-note">Locations from the fraud dataset</div>
1005
+ </div>
1006
+ </div>
1007
+
1008
+ <div class="form-row">
1009
+ <div class="form-group">
1010
+ <label for="merchantName">Merchant Name</label>
1011
+ <input type="text" id="merchantName" class="form-control" placeholder="Enter merchant name">
1012
+ </div>
1013
+
1014
+ <div class="form-group">
1015
+ <label for="transactionCategory">Transaction Category</label>
1016
+ <select id="transactionCategory" class="form-control">
1017
+ <option value="retail">Retail</option>
1018
+ <option value="travel">Travel</option>
1019
+ <option value="food">Food & Dining</option>
1020
+ <option value="entertainment">Entertainment</option>
1021
+ <option value="digital">Digital Products</option>
1022
+ <option value="other">Other</option>
1023
+ </select>
1024
+ </div>
1025
+ </div>
1026
+
1027
+ <div class="form-row">
1028
+ <div class="form-group">
1029
+ <label for="customerEmail">Customer Email Domain</label>
1030
+ <select id="customerEmail" class="form-control">
1031
+ <option value="gmail.com">gmail.com</option>
1032
+ <option value="yahoo.com">yahoo.com</option>
1033
+ <option value="outlook.com">outlook.com</option>
1034
+ <option value="company.com">company.com</option>
1035
+ <option value="other">Other Domain</option>
1036
+ </select>
1037
+ <div class="form-note">From dataset: customerEmail is an important feature</div>
1038
+ </div>
1039
+
1040
+ <div class="form-group">
1041
+ <label for="customerDevice">Customer Device</label>
1042
+ <select id="customerDevice" class="form-control">
1043
+ <option value="mobile">Mobile</option>
1044
+ <option value="desktop">Desktop</option>
1045
+ <option value="tablet">Tablet</option>
1046
+ <option value="unknown">Unknown</option>
1047
+ </select>
1048
+ <div class="form-note">From dataset: customerDevice is used for fraud detection</div>
1049
+ </div>
1050
+ </div>
1051
+
1052
+ <div class="form-group">
1053
+ <label for="transactionType">Transaction Type</label>
1054
+ <select id="transactionType" class="form-control">
1055
+ <option value="online">Online Purchase</option>
1056
+ <option value="pos">Point of Sale</option>
1057
+ <option value="atm">ATM Withdrawal</option>
1058
+ <option value="transfer">Bank Transfer</option>
1059
+ </select>
1060
+ </div>
1061
+
1062
+ <button type="submit" class="btn btn-primary btn-block" id="predictButton">
1063
+ <i class="fas fa-brain"></i> Analyze Transaction for Fraud
1064
+ </button>
1065
+ </form>
1066
+ </div>
1067
+
1068
+ <!-- Results Container -->
1069
+ <div class="results-container" id="resultsContainer">
1070
+ <div class="results-header">
1071
+ <h3 class="results-title">Prediction Results</h3>
1072
+ <div class="prediction-badge" id="predictionBadge">Safe</div>
1073
+ </div>
1074
+
1075
+ <div class="prediction-details">
1076
+ <div class="detail-box">
1077
+ <div class="detail-label">Transaction Amount</div>
1078
+ <div class="detail-value" id="resultAmount">$0.00</div>
1079
+ </div>
1080
+
1081
+ <div class="detail-box">
1082
+ <div class="detail-label">Location</div>
1083
+ <div class="detail-value" id="resultLocation">Unknown</div>
1084
+ </div>
1085
+
1086
+ <div class="detail-box">
1087
+ <div class="detail-label">Merchant</div>
1088
+ <div class="detail-value" id="resultMerchant">Unknown</div>
1089
+ </div>
1090
+
1091
+ <div class="detail-box">
1092
+ <div class="detail-label">Prediction Confidence</div>
1093
+ <div class="detail-value" id="resultConfidence">0%</div>
1094
+ </div>
1095
+ </div>
1096
+
1097
+ <div class="probability-container">
1098
+ <div class="probability-header">
1099
+ <span>Fraud Probability</span>
1100
+ <span id="probabilityValue">0%</span>
1101
+ </div>
1102
+ <div class="probability-bar">
1103
+ <div class="probability-fill" id="probabilityFill" style="width: 0%;"></div>
1104
+ </div>
1105
+ <div class="probability-text">
1106
+ <span>Legitimate Transaction</span>
1107
+ <span>Fraudulent Transaction</span>
1108
+ </div>
1109
+ </div>
1110
+
1111
+ <div class="feedback-section">
1112
+ <h4 class="feedback-title">Is this prediction accurate?</h4>
1113
+ <p style="margin-bottom: 20px; color: #666;">Your feedback helps improve the AI model</p>
1114
+
1115
+ <div class="feedback-buttons">
1116
+ <button class="feedback-btn" id="feedbackAccurate">
1117
+ <i class="fas fa-check-circle"></i>
1118
+ <span>Accurate Prediction</span>
1119
+ </button>
1120
+ <button class="feedback-btn" id="feedbackInaccurate">
1121
+ <i class="fas fa-times-circle"></i>
1122
+ <span>Inaccurate Prediction</span>
1123
+ </button>
1124
+ </div>
1125
+ </div>
1126
+ </div>
1127
+ </div>
1128
+
1129
+ <!-- Transactions History Page -->
1130
+ <div id="transactions-page" class="page">
1131
+ <div class="section-header">
1132
+ <h2 class="section-title"><i class="fas fa-history"></i> Transaction History</h2>
1133
+ <div class="table-actions">
1134
+ <button class="btn btn-primary" id="refreshTransactions">
1135
+ <i class="fas fa-sync-alt"></i> Refresh
1136
+ </button>
1137
+ <button class="btn btn-success" id="exportTransactions">
1138
+ <i class="fas fa-file-export"></i> Export CSV
1139
+ </button>
1140
+ </div>
1141
+ </div>
1142
+
1143
+ <div class="transactions-container">
1144
+ <div class="filters" style="margin-bottom: 20px; display: flex; gap: 15px; flex-wrap: wrap;">
1145
+ <select id="filterStatus" class="form-control" style="width: auto;">
1146
+ <option value="">All Status</option>
1147
+ <option value="fraud">Fraud</option>
1148
+ <option value="safe">Safe</option>
1149
+ </select>
1150
+
1151
+ <input type="date" id="filterDate" class="form-control" style="width: auto;">
1152
+
1153
+ <input type="text" id="searchTransaction" class="form-control" placeholder="Search transactions..." style="flex-grow: 1;">
1154
+ </div>
1155
+
1156
+ <table id="transactionsTable">
1157
+ <thead>
1158
+ <tr>
1159
+ <th>ID</th>
1160
+ <th>Date & Time</th>
1161
+ <th>Amount</th>
1162
+ <th>Location</th>
1163
+ <th>Merchant</th>
1164
+ <th>Category</th>
1165
+ <th>Status</th>
1166
+ <th>Confidence</th>
1167
+ <th>Actions</th>
1168
+ </tr>
1169
+ </thead>
1170
+ <tbody id="transactionsBody">
1171
+ <!-- Transactions will be loaded here -->
1172
+ </tbody>
1173
+ </table>
1174
+
1175
+ <div class="table-controls">
1176
+ <div class="table-info" id="tableInfo">Showing 0 transactions</div>
1177
+ <div class="pagination">
1178
+ <button class="btn" id="prevPage" disabled><i class="fas fa-chevron-left"></i> Previous</button>
1179
+ <span style="margin: 0 15px;" id="pageInfo">Page 1 of 1</span>
1180
+ <button class="btn" id="nextPage" disabled>Next <i class="fas fa-chevron-right"></i></button>
1181
+ </div>
1182
+ </div>
1183
+ </div>
1184
+ </div>
1185
+
1186
+ <!-- Feature Importance Page -->
1187
+ <div id="features-page" class="page">
1188
+ <div class="section-header">
1189
+ <h2 class="section-title"><i class="fas fa-chart-bar"></i> Feature Importance Analysis</h2>
1190
+ <div class="model-info" style="background-color: #e8f4fc; padding: 10px 20px; border-radius: var(--border-radius);">
1191
+ <span style="font-weight: 600; color: var(--secondary-color);">
1192
+ <i class="fas fa-project-diagram"></i> Based on Random Forest Feature Importance
1193
+ </span>
1194
+ </div>
1195
+ </div>
1196
+
1197
+ <div class="feature-importance-container">
1198
+ <h3 style="margin-bottom: 20px; color: var(--primary-color);">Top 10 Most Important Features for Fraud Detection</h3>
1199
+
1200
+ <div class="chart-wrapper">
1201
+ <canvas id="featureImportanceChart"></canvas>
1202
+ </div>
1203
+
1204
+ <div class="feature-list" id="featureList">
1205
+ <!-- Feature importance list will be loaded here -->
1206
+ </div>
1207
+ </div>
1208
+
1209
+ <div class="charts-container">
1210
+ <div class="chart-card">
1211
+ <h3 class="chart-title"><i class="fas fa-money-bill-wave"></i> Amount Distribution</h3>
1212
+ <div class="chart-wrapper">
1213
+ <canvas id="amountDistributionChart"></canvas>
1214
+ </div>
1215
+ <div style="margin-top: 15px; font-size: 0.9rem; color: #666;">
1216
+ <p>Dataset statistics: Amount ranges from $4.30 to $4189.27</p>
1217
+ <p>Average fraud amount: $1,245.67 | Average legitimate amount: $256.43</p>
1218
+ </div>
1219
+ </div>
1220
+
1221
+ <div class="chart-card">
1222
+ <h3 class="chart-title"><i class="fas fa-map-marker-alt"></i> Fraud by Location Heatmap</h3>
1223
+ <div class="chart-wrapper">
1224
+ <canvas id="locationHeatmapChart"></canvas>
1225
+ </div>
1226
+ <div style="margin-top: 15px; font-size: 0.9rem; color: #666;">
1227
+ <p>Top 5 fraud locations: New York, Dallas, Phoenix, San Antonio, Philadelphia</p>
1228
+ </div>
1229
+ </div>
1230
+ </div>
1231
+ </div>
1232
+ </div>
1233
+
1234
+ <script>
1235
+ // Base URL for Flask API - can be overridden at runtime via the header input.
1236
+ // NOTE: backend exposes `/predict` at the server root, not under /api
1237
+ let API_BASE_URL = localStorage.getItem('API_BASE_URL') || 'http://localhost:5000';
1238
+
1239
+ // State management
1240
+ let currentUser = {
1241
+ id: 1,
1242
+ name: "Admin User",
1243
+ email: "admin@frauddetection.com"
1244
+ };
1245
+
1246
+ let transactions = [];
1247
+ let currentPage = 1;
1248
+ const transactionsPerPage = 10;
1249
+
1250
+ // DOM Elements
1251
+ const pages = {
1252
+ dashboard: document.getElementById('dashboard-page'),
1253
+ predict: document.getElementById('predict-page'),
1254
+ transactions: document.getElementById('transactions-page'),
1255
+ features: document.getElementById('features-page')
1256
+ };
1257
+
1258
+ const navLinks = document.querySelectorAll('.nav-link');
1259
+ const logoutBtn = document.getElementById('logoutBtn');
1260
+ const predictionForm = document.getElementById('predictionForm');
1261
+ const predictButton = document.getElementById('predictButton');
1262
+ const resultsContainer = document.getElementById('resultsContainer');
1263
+ const probabilityFill = document.getElementById('probabilityFill');
1264
+ const probabilityValue = document.getElementById('probabilityValue');
1265
+ const predictionBadge = document.getElementById('predictionBadge');
1266
+ const feedbackAccurate = document.getElementById('feedbackAccurate');
1267
+ const feedbackInaccurate = document.getElementById('feedbackInaccurate');
1268
+ const refreshTransactionsBtn = document.getElementById('refreshTransactions');
1269
+ const exportTransactionsBtn = document.getElementById('exportTransactions');
1270
+ const filterStatus = document.getElementById('filterStatus');
1271
+ const filterDate = document.getElementById('filterDate');
1272
+ const searchTransaction = document.getElementById('searchTransaction');
1273
+ const transactionsBody = document.getElementById('transactionsBody');
1274
+ const prevPageBtn = document.getElementById('prevPage');
1275
+ const nextPageBtn = document.getElementById('nextPage');
1276
+ const pageInfo = document.getElementById('pageInfo');
1277
+ const tableInfo = document.getElementById('tableInfo');
1278
+
1279
+ // Chart instances
1280
+ let fraudDistributionChart, locationFraudChart, featureImportanceChart, amountDistributionChart, locationHeatmapChart;
1281
+
1282
+ // Initialize the application
1283
+ document.addEventListener('DOMContentLoaded', function() {
1284
+ initializeEventListeners();
1285
+ loadDashboardData();
1286
+ initializeCharts();
1287
+ loadSampleTransactions();
1288
+ loadFeatureImportance();
1289
+
1290
+ // Simulate user login
1291
+ simulateLogin();
1292
+
1293
+ // Populate backend URL input from saved value
1294
+ const backendUrlInput = document.getElementById('backendUrlInput');
1295
+ const setBackendUrlBtn = document.getElementById('setBackendUrlBtn');
1296
+ if (backendUrlInput) backendUrlInput.value = localStorage.getItem('API_BASE_URL') || API_BASE_URL;
1297
+
1298
+ if (setBackendUrlBtn) {
1299
+ setBackendUrlBtn.addEventListener('click', function() {
1300
+ const val = (backendUrlInput && backendUrlInput.value || '').trim();
1301
+ if (!val) {
1302
+ localStorage.removeItem('API_BASE_URL');
1303
+ API_BASE_URL = 'http://localhost:5000';
1304
+ showNotification('warning', 'Backend URL cleared — using localhost');
1305
+ return;
1306
+ }
1307
+ try {
1308
+ new URL(val);
1309
+ } catch (e) {
1310
+ showNotification('danger', 'Invalid URL');
1311
+ return;
1312
+ }
1313
+ localStorage.setItem('API_BASE_URL', val);
1314
+ API_BASE_URL = val;
1315
+ showNotification('success', 'Backend URL saved');
1316
+ });
1317
+ }
1318
+
1319
+ // Register service worker for PWA (if available)
1320
+ if ('serviceWorker' in navigator) {
1321
+ navigator.serviceWorker.register('/sw.js')
1322
+ .then(reg => console.log('ServiceWorker registered', reg.scope))
1323
+ .catch(err => console.warn('ServiceWorker registration failed', err));
1324
+ }
1325
+ });
1326
+
1327
+ function initializeEventListeners() {
1328
+ // Navigation
1329
+ navLinks.forEach(link => {
1330
+ link.addEventListener('click', function(e) {
1331
+ e.preventDefault();
1332
+ const page = this.getAttribute('data-page');
1333
+ switchPage(page);
1334
+ });
1335
+ });
1336
+
1337
+ // Logout
1338
+ logoutBtn.addEventListener('click', function() {
1339
+ showNotification('success', 'Logged out successfully');
1340
+ setTimeout(() => {
1341
+ window.location.reload();
1342
+ }, 1500);
1343
+ });
1344
+
1345
+ // Prediction form
1346
+ predictionForm.addEventListener('submit', function(e) {
1347
+ e.preventDefault();
1348
+ predictFraud();
1349
+ });
1350
+
1351
+ // Feedback buttons
1352
+ feedbackAccurate.addEventListener('click', function() {
1353
+ submitFeedback(true);
1354
+ this.classList.add('active');
1355
+ feedbackInaccurate.classList.remove('active');
1356
+ });
1357
+
1358
+ feedbackInaccurate.addEventListener('click', function() {
1359
+ submitFeedback(false);
1360
+ this.classList.add('active');
1361
+ feedbackAccurate.classList.remove('active');
1362
+ });
1363
+
1364
+ // Transactions
1365
+ refreshTransactionsBtn.addEventListener('click', loadSampleTransactions);
1366
+ exportTransactionsBtn.addEventListener('click', exportTransactionsToCSV);
1367
+ filterStatus.addEventListener('change', filterTransactions);
1368
+ filterDate.addEventListener('change', filterTransactions);
1369
+ searchTransaction.addEventListener('input', filterTransactions);
1370
+ prevPageBtn.addEventListener('click', () => changePage(-1));
1371
+ nextPageBtn.addEventListener('click', () => changePage(1));
1372
+ }
1373
+
1374
+ function switchPage(pageName) {
1375
+ // Hide all pages
1376
+ Object.values(pages).forEach(page => {
1377
+ page.classList.remove('active');
1378
+ });
1379
+
1380
+ // Show selected page
1381
+ pages[pageName].classList.add('active');
1382
+
1383
+ // Update active nav link
1384
+ navLinks.forEach(link => {
1385
+ link.classList.remove('active');
1386
+ if (link.getAttribute('data-page') === pageName) {
1387
+ link.classList.add('active');
1388
+ }
1389
+ });
1390
+
1391
+ // Load data for specific pages
1392
+ if (pageName === 'dashboard') {
1393
+ updateDashboardStats();
1394
+ } else if (pageName === 'transactions') {
1395
+ renderTransactionsTable();
1396
+ }
1397
+ }
1398
+
1399
+ // Simulate login - in real app, this would be an API call
1400
+ function simulateLogin() {
1401
+ showNotification('success', `Welcome back, ${currentUser.name}!`);
1402
+ }
1403
+
1404
+ // Load dashboard data
1405
+ function loadDashboardData() {
1406
+ // In a real app, this would be an API call
1407
+ updateDashboardStats();
1408
+ }
1409
+
1410
+ function updateDashboardStats() {
1411
+ // Simulate API response
1412
+ const stats = {
1413
+ totalFraud: Math.floor(Math.random() * 500) + 120,
1414
+ totalSafe: Math.floor(Math.random() * 10000) + 8500,
1415
+ totalTransactions: Math.floor(Math.random() * 10500) + 9000,
1416
+ accuracyRate: (Math.random() * 10 + 90).toFixed(1) + '%'
1417
+ };
1418
+
1419
+ document.getElementById('totalFraud').textContent = stats.totalFraud;
1420
+ document.getElementById('totalSafe').textContent = stats.totalSafe;
1421
+ document.getElementById('totalTransactions').textContent = stats.totalTransactions;
1422
+ document.getElementById('accuracyRate').textContent = stats.accuracyRate;
1423
+ }
1424
+
1425
+ // Initialize charts
1426
+ function initializeCharts() {
1427
+ // Fraud Distribution Chart
1428
+ const fraudDistributionCtx = document.getElementById('fraudDistributionChart').getContext('2d');
1429
+ fraudDistributionChart = new Chart(fraudDistributionCtx, {
1430
+ type: 'doughnut',
1431
+ data: {
1432
+ labels: ['Legitimate', 'Fraudulent'],
1433
+ datasets: [{
1434
+ data: [92, 8],
1435
+ backgroundColor: [
1436
+ 'rgba(39, 174, 96, 0.8)',
1437
+ 'rgba(231, 76, 60, 0.8)'
1438
+ ],
1439
+ borderColor: [
1440
+ 'rgba(39, 174, 96, 1)',
1441
+ 'rgba(231, 76, 60, 1)'
1442
+ ],
1443
+ borderWidth: 2
1444
+ }]
1445
+ },
1446
+ options: {
1447
+ responsive: true,
1448
+ maintainAspectRatio: false,
1449
+ plugins: {
1450
+ legend: {
1451
+ position: 'bottom'
1452
+ },
1453
+ tooltip: {
1454
+ callbacks: {
1455
+ label: function(context) {
1456
+ return `${context.label}: ${context.raw}%`;
1457
+ }
1458
+ }
1459
+ }
1460
+ }
1461
+ }
1462
+ });
1463
+
1464
+ // Location Fraud Chart
1465
+ const locationFraudCtx = document.getElementById('locationFraudChart').getContext('2d');
1466
+ locationFraudChart = new Chart(locationFraudCtx, {
1467
+ type: 'bar',
1468
+ data: {
1469
+ labels: ['New York', 'Dallas', 'Phoenix', 'San Antonio', 'Philadelphia', 'Utah', 'Maryland'],
1470
+ datasets: [
1471
+ {
1472
+ label: 'Fraudulent',
1473
+ data: [45, 38, 32, 28, 24, 15, 12],
1474
+ backgroundColor: 'rgba(231, 76, 60, 0.8)',
1475
+ borderColor: 'rgba(231, 76, 60, 1)',
1476
+ borderWidth: 1
1477
+ },
1478
+ {
1479
+ label: 'Legitimate',
1480
+ data: [455, 412, 398, 322, 376, 285, 288],
1481
+ backgroundColor: 'rgba(39, 174, 96, 0.8)',
1482
+ borderColor: 'rgba(39, 174, 96, 1)',
1483
+ borderWidth: 1
1484
+ }
1485
+ ]
1486
+ },
1487
+ options: {
1488
+ responsive: true,
1489
+ maintainAspectRatio: false,
1490
+ scales: {
1491
+ x: {
1492
+ grid: {
1493
+ display: false
1494
+ }
1495
+ },
1496
+ y: {
1497
+ beginAtZero: true,
1498
+ ticks: {
1499
+ callback: function(value) {
1500
+ return value;
1501
+ }
1502
+ }
1503
+ }
1504
+ },
1505
+ plugins: {
1506
+ legend: {
1507
+ position: 'top'
1508
+ }
1509
+ }
1510
+ }
1511
+ });
1512
+ }
1513
+
1514
+ // Load feature importance data
1515
+ function loadFeatureImportance() {
1516
+ // Feature importance data based on the provided dataset columns
1517
+ const featureImportanceData = [
1518
+ { feature: 'transactionAmount', importance: 0.28 },
1519
+ { feature: 'customerDevice', importance: 0.18 },
1520
+ { feature: 'location', importance: 0.15 },
1521
+ { feature: 'customerEmail', importance: 0.12 },
1522
+ { feature: 'No_Transactions', importance: 0.08 },
1523
+ { feature: 'merchant', importance: 0.07 },
1524
+ { feature: 'category', importance: 0.05 },
1525
+ { feature: 'customerIPAddress', importance: 0.04 },
1526
+ { feature: 'TransactionType', importance: 0.02 },
1527
+ { feature: 'customerBillingAddress', importance: 0.01 }
1528
+ ];
1529
+
1530
+ // Render feature importance list
1531
+ const featureList = document.getElementById('featureList');
1532
+ featureList.innerHTML = '';
1533
+
1534
+ featureImportanceData.forEach((item, index) => {
1535
+ const featureItem = document.createElement('div');
1536
+ featureItem.className = 'feature-item';
1537
+ featureItem.innerHTML = `
1538
+ <div class="feature-rank">${index + 1}</div>
1539
+ <div class="feature-name">${formatFeatureName(item.feature)}</div>
1540
+ <div class="feature-importance">${(item.importance * 100).toFixed(1)}%</div>
1541
+ `;
1542
+ featureList.appendChild(featureItem);
1543
+ });
1544
+
1545
+ // Create feature importance chart
1546
+ const featureImportanceCtx = document.getElementById('featureImportanceChart').getContext('2d');
1547
+ featureImportanceChart = new Chart(featureImportanceCtx, {
1548
+ type: 'horizontalBar',
1549
+ data: {
1550
+ labels: featureImportanceData.map(item => formatFeatureName(item.feature)),
1551
+ datasets: [{
1552
+ label: 'Importance',
1553
+ data: featureImportanceData.map(item => item.importance * 100),
1554
+ backgroundColor: 'rgba(52, 152, 219, 0.8)',
1555
+ borderColor: 'rgba(52, 152, 219, 1)',
1556
+ borderWidth: 1
1557
+ }]
1558
+ },
1559
+ options: {
1560
+ responsive: true,
1561
+ maintainAspectRatio: false,
1562
+ indexAxis: 'y',
1563
+ scales: {
1564
+ x: {
1565
+ beginAtZero: true,
1566
+ ticks: {
1567
+ callback: function(value) {
1568
+ return value + '%';
1569
+ }
1570
+ }
1571
+ }
1572
+ },
1573
+ plugins: {
1574
+ legend: {
1575
+ display: false
1576
+ },
1577
+ tooltip: {
1578
+ callbacks: {
1579
+ label: function(context) {
1580
+ return `Importance: ${context.raw.toFixed(1)}%`;
1581
+ }
1582
+ }
1583
+ }
1584
+ }
1585
+ }
1586
+ });
1587
+
1588
+ // Create amount distribution chart
1589
+ const amountDistributionCtx = document.getElementById('amountDistributionChart').getContext('2d');
1590
+ amountDistributionChart = new Chart(amountDistributionCtx, {
1591
+ type: 'scatter',
1592
+ data: {
1593
+ datasets: [
1594
+ {
1595
+ label: 'Legitimate Transactions',
1596
+ data: generateRandomAmountData(200, 4, 500, false),
1597
+ backgroundColor: 'rgba(39, 174, 96, 0.6)',
1598
+ borderColor: 'rgba(39, 174, 96, 1)',
1599
+ pointRadius: 5
1600
+ },
1601
+ {
1602
+ label: 'Fraudulent Transactions',
1603
+ data: generateRandomAmountData(50, 300, 4200, true),
1604
+ backgroundColor: 'rgba(231, 76, 60, 0.6)',
1605
+ borderColor: 'rgba(231, 76, 60, 1)',
1606
+ pointRadius: 6
1607
+ }
1608
+ ]
1609
+ },
1610
+ options: {
1611
+ responsive: true,
1612
+ maintainAspectRatio: false,
1613
+ scales: {
1614
+ x: {
1615
+ type: 'linear',
1616
+ position: 'bottom',
1617
+ title: {
1618
+ display: true,
1619
+ text: 'Transaction Amount ($)'
1620
+ }
1621
+ },
1622
+ y: {
1623
+ title: {
1624
+ display: true,
1625
+ text: 'Frequency'
1626
+ },
1627
+ ticks: {
1628
+ display: false
1629
+ }
1630
+ }
1631
+ },
1632
+ plugins: {
1633
+ legend: {
1634
+ position: 'top'
1635
+ }
1636
+ }
1637
+ }
1638
+ });
1639
+
1640
+ // Create location heatmap chart
1641
+ const locationHeatmapCtx = document.getElementById('locationHeatmapChart').getContext('2d');
1642
+ locationHeatmapChart = new Chart(locationHeatmapCtx, {
1643
+ type: 'radar',
1644
+ data: {
1645
+ labels: ['New York', 'Dallas', 'Phoenix', 'San Antonio', 'Philadelphia', 'Utah', 'Maryland'],
1646
+ datasets: [
1647
+ {
1648
+ label: 'Fraud Risk Level',
1649
+ data: [85, 78, 72, 65, 58, 42, 38],
1650
+ backgroundColor: 'rgba(231, 76, 60, 0.2)',
1651
+ borderColor: 'rgba(231, 76, 60, 1)',
1652
+ pointBackgroundColor: 'rgba(231, 76, 60, 1)',
1653
+ pointBorderColor: '#fff',
1654
+ pointHoverBackgroundColor: '#fff',
1655
+ pointHoverBorderColor: 'rgba(231, 76, 60, 1)'
1656
+ }
1657
+ ]
1658
+ },
1659
+ options: {
1660
+ responsive: true,
1661
+ maintainAspectRatio: false,
1662
+ scales: {
1663
+ r: {
1664
+ angleLines: {
1665
+ display: true
1666
+ },
1667
+ suggestedMin: 0,
1668
+ suggestedMax: 100
1669
+ }
1670
+ },
1671
+ plugins: {
1672
+ legend: {
1673
+ display: false
1674
+ }
1675
+ }
1676
+ }
1677
+ });
1678
+ }
1679
+
1680
+ function formatFeatureName(feature) {
1681
+ // Format feature names for display
1682
+ const nameMap = {
1683
+ 'transactionAmount': 'Transaction Amount',
1684
+ 'customerDevice': 'Customer Device',
1685
+ 'location': 'Location',
1686
+ 'customerEmail': 'Customer Email',
1687
+ 'No_Transactions': 'Number of Transactions',
1688
+ 'merchant': 'Merchant',
1689
+ 'category': 'Category',
1690
+ 'customerIPAddress': 'Customer IP Address',
1691
+ 'TransactionType': 'Transaction Type',
1692
+ 'customerBillingAddress': 'Billing Address'
1693
+ };
1694
+
1695
+ return nameMap[feature] || feature.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
1696
+ }
1697
+
1698
+ function generateRandomAmountData(count, min, max, isFraud) {
1699
+ const data = [];
1700
+ for (let i = 0; i < count; i++) {
1701
+ const amount = Math.random() * (max - min) + min;
1702
+ const y = isFraud ? Math.random() * 0.5 + 0.5 : Math.random() * 0.5;
1703
+ data.push({ x: amount, y: y });
1704
+ }
1705
+ return data;
1706
+ }
1707
+
1708
+ // Load sample transactions
1709
+ function loadSampleTransactions() {
1710
+ // Generate sample transactions based on the dataset
1711
+ const locations = ['New York', 'Dallas', 'Phoenix', 'San Antonio', 'Philadelphia', 'Utah', 'Maryland', 'New Mexico', 'South Dakota', 'Montana'];
1712
+ const merchants = ['Amazon', 'Walmart', 'Target', 'Best Buy', 'Apple', 'Starbucks', 'Uber', 'Airbnb', 'Netflix', 'Spotify'];
1713
+ const categories = ['retail', 'travel', 'food', 'entertainment', 'digital'];
1714
+
1715
+ transactions = [];
1716
+
1717
+ for (let i = 1; i <= 50; i++) {
1718
+ const amount = (Math.random() * 4000 + 4.3).toFixed(2);
1719
+ const location = locations[Math.floor(Math.random() * locations.length)];
1720
+ const isFraud = Math.random() < 0.08; // 8% chance of fraud
1721
+ const confidence = (Math.random() * 30 + 70).toFixed(1); // 70-100% confidence
1722
+
1723
+ const date = new Date();
1724
+ date.setDate(date.getDate() - Math.floor(Math.random() * 30));
1725
+
1726
+ transactions.push({
1727
+ id: `TRX${10000 + i}`,
1728
+ date: date.toISOString(),
1729
+ amount: parseFloat(amount),
1730
+ location: location,
1731
+ merchant: merchants[Math.floor(Math.random() * merchants.length)],
1732
+ category: categories[Math.floor(Math.random() * categories.length)],
1733
+ isFraud: isFraud,
1734
+ confidence: parseFloat(confidence),
1735
+ status: isFraud ? 'fraud' : 'safe'
1736
+ });
1737
+ }
1738
+
1739
+ // Sort by date (newest first)
1740
+ transactions.sort((a, b) => new Date(b.date) - new Date(a.date));
1741
+
1742
+ renderTransactionsTable();
1743
+ showNotification('success', 'Transactions loaded successfully');
1744
+ }
1745
+
1746
+ function renderTransactionsTable() {
1747
+ // Filter transactions
1748
+ let filteredTransactions = [...transactions];
1749
+
1750
+ // Apply status filter
1751
+ if (filterStatus.value) {
1752
+ filteredTransactions = filteredTransactions.filter(t => t.status === filterStatus.value);
1753
+ }
1754
+
1755
+ // Apply date filter
1756
+ if (filterDate.value) {
1757
+ const filterDateObj = new Date(filterDate.value);
1758
+ filteredTransactions = filteredTransactions.filter(t => {
1759
+ const transactionDate = new Date(t.date);
1760
+ return transactionDate.toDateString() === filterDateObj.toDateString();
1761
+ });
1762
+ }
1763
+
1764
+ // Apply search filter
1765
+ if (searchTransaction.value) {
1766
+ const searchTerm = searchTransaction.value.toLowerCase();
1767
+ filteredTransactions = filteredTransactions.filter(t =>
1768
+ t.id.toLowerCase().includes(searchTerm) ||
1769
+ t.merchant.toLowerCase().includes(searchTerm) ||
1770
+ t.location.toLowerCase().includes(searchTerm)
1771
+ );
1772
+ }
1773
+
1774
+ // Calculate pagination
1775
+ const totalPages = Math.ceil(filteredTransactions.length / transactionsPerPage);
1776
+ const startIndex = (currentPage - 1) * transactionsPerPage;
1777
+ const endIndex = Math.min(startIndex + transactionsPerPage, filteredTransactions.length);
1778
+ const pageTransactions = filteredTransactions.slice(startIndex, endIndex);
1779
+
1780
+ // Update table
1781
+ transactionsBody.innerHTML = '';
1782
+
1783
+ if (pageTransactions.length === 0) {
1784
+ const row = document.createElement('tr');
1785
+ row.innerHTML = `
1786
+ <td colspan="9" style="text-align: center; padding: 40px; color: #666;">
1787
+ <i class="fas fa-search" style="font-size: 2rem; margin-bottom: 15px; display: block;"></i>
1788
+ No transactions found matching your criteria
1789
+ </td>
1790
+ `;
1791
+ transactionsBody.appendChild(row);
1792
+ } else {
1793
+ pageTransactions.forEach(transaction => {
1794
+ const row = document.createElement('tr');
1795
+ const date = new Date(transaction.date);
1796
+ const formattedDate = date.toLocaleDateString('en-US', {
1797
+ year: 'numeric',
1798
+ month: 'short',
1799
+ day: 'numeric',
1800
+ hour: '2-digit',
1801
+ minute: '2-digit'
1802
+ });
1803
+
1804
+ row.innerHTML = `
1805
+ <td>${transaction.id}</td>
1806
+ <td>${formattedDate}</td>
1807
+ <td>$${transaction.amount.toFixed(2)}</td>
1808
+ <td>${transaction.location}</td>
1809
+ <td>${transaction.merchant}</td>
1810
+ <td>${transaction.category}</td>
1811
+ <td>
1812
+ <span class="status-badge ${transaction.isFraud ? 'status-fraud' : 'status-safe'}">
1813
+ ${transaction.isFraud ? 'FRAUD' : 'SAFE'}
1814
+ </span>
1815
+ </td>
1816
+ <td>${transaction.confidence}%</td>
1817
+ <td>
1818
+ <button class="btn" style="padding: 5px 10px; font-size: 0.85rem;" onclick="viewTransaction('${transaction.id}')">
1819
+ <i class="fas fa-eye"></i> View
1820
+ </button>
1821
+ </td>
1822
+ `;
1823
+ transactionsBody.appendChild(row);
1824
+ });
1825
+ }
1826
+
1827
+ // Update pagination controls
1828
+ prevPageBtn.disabled = currentPage <= 1;
1829
+ nextPageBtn.disabled = currentPage >= totalPages;
1830
+
1831
+ pageInfo.textContent = `Page ${currentPage} of ${totalPages || 1}`;
1832
+ tableInfo.textContent = `Showing ${startIndex + 1}-${endIndex} of ${filteredTransactions.length} transactions`;
1833
+ }
1834
+
1835
+ function changePage(direction) {
1836
+ const filteredTransactions = getFilteredTransactions();
1837
+ const totalPages = Math.ceil(filteredTransactions.length / transactionsPerPage);
1838
+
1839
+ currentPage += direction;
1840
+
1841
+ if (currentPage < 1) currentPage = 1;
1842
+ if (currentPage > totalPages) currentPage = totalPages;
1843
+
1844
+ renderTransactionsTable();
1845
+ }
1846
+
1847
+ function getFilteredTransactions() {
1848
+ let filteredTransactions = [...transactions];
1849
+
1850
+ if (filterStatus.value) {
1851
+ filteredTransactions = filteredTransactions.filter(t => t.status === filterStatus.value);
1852
+ }
1853
+
1854
+ if (filterDate.value) {
1855
+ const filterDateObj = new Date(filterDate.value);
1856
+ filteredTransactions = filteredTransactions.filter(t => {
1857
+ const transactionDate = new Date(t.date);
1858
+ return transactionDate.toDateString() === filterDateObj.toDateString();
1859
+ });
1860
+ }
1861
+
1862
+ if (searchTransaction.value) {
1863
+ const searchTerm = searchTransaction.value.toLowerCase();
1864
+ filteredTransactions = filteredTransactions.filter(t =>
1865
+ t.id.toLowerCase().includes(searchTerm) ||
1866
+ t.merchant.toLowerCase().includes(searchTerm) ||
1867
+ t.location.toLowerCase().includes(searchTerm)
1868
+ );
1869
+ }
1870
+
1871
+ return filteredTransactions;
1872
+ }
1873
+
1874
+ function filterTransactions() {
1875
+ currentPage = 1;
1876
+ renderTransactionsTable();
1877
+ }
1878
+
1879
+ // Predict fraud - simulate API call to Flask backend
1880
+ function predictFraud() {
1881
+ const amount = parseFloat(document.getElementById('transactionAmount').value);
1882
+ const location = document.getElementById('transactionLocation').value;
1883
+ const merchant = document.getElementById('merchantName').value || 'Unknown Merchant';
1884
+ const category = document.getElementById('transactionCategory').value;
1885
+ const email = document.getElementById('customerEmail').value;
1886
+ const device = document.getElementById('customerDevice').value;
1887
+ const type = document.getElementById('transactionType').value;
1888
+
1889
+ // Show loading state
1890
+ predictButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Analyzing...';
1891
+ predictButton.disabled = true;
1892
+
1893
+ // Build a payload that matches the preprocessor expected feature names.
1894
+ // The preprocessor expects a fixed set of features (see preprocessor.feature_names_in_).
1895
+ // We'll provide best-effort mappings from the form and sensible defaults for missing values.
1896
+ const now = new Date();
1897
+ const payload = {
1898
+ merchant_name: merchant || '',
1899
+ avg_amount_per_transaction: amount || 0,
1900
+ day_of_week: now.getDay(),
1901
+ amount_deviation_from_location_mean: 0,
1902
+ transaction_category: category || '',
1903
+ customer_no_transactions: 0,
1904
+ customer_lat: null,
1905
+ transaction_type: type || '',
1906
+ customer_place_name: null,
1907
+ merchant_id: null,
1908
+ location: location || '',
1909
+ customer_job: null,
1910
+ age: null,
1911
+ merchant_long: null,
1912
+ amount_per_city_pop: 0,
1913
+ customer_long: null,
1914
+ distance_customer_merchant: 0,
1915
+ transactions_per_customer_ratio: 0,
1916
+ customer_city_population: 0,
1917
+ merchant_lat: null,
1918
+ customer_no_payments: 0,
1919
+ customer_no_orders: 0,
1920
+ payments_per_order_ratio: 0,
1921
+ hour_of_day: now.getHours(),
1922
+ amount: amount || 0,
1923
+ customer_zip_code: null,
1924
+ mean_amount_by_location: 0,
1925
+ fraud_rate_by_location: 0,
1926
+ customer_gender: null
1927
+ };
1928
+
1929
+ // Real network call to backend
1930
+ fetch(`${API_BASE_URL}/predict`, {
1931
+ method: 'POST',
1932
+ headers: {
1933
+ 'Content-Type': 'application/json',
1934
+ },
1935
+ body: JSON.stringify(payload),
1936
+ })
1937
+ .then(async response => {
1938
+ if (!response.ok) {
1939
+ // Try to read JSON error if available
1940
+ let errText = `${response.status} ${response.statusText}`;
1941
+ try {
1942
+ const errBody = await response.json();
1943
+ if (errBody && errBody.error) errText = errBody.error;
1944
+ } catch (e) {}
1945
+ throw new Error(errText);
1946
+ }
1947
+ return response.json();
1948
+ })
1949
+ .then(data => {
1950
+ // Normalize different shapes (legacy, mock, or production)
1951
+ displayPredictionResults(data, amount, location, merchant);
1952
+ })
1953
+ .catch(error => {
1954
+ console.error('Prediction API error:', error);
1955
+ showNotification('danger', `Prediction service error: ${error.message}`);
1956
+
1957
+ // Fallback to local simulation so UI remains usable offline
1958
+ const isFraud = simulateFraudPrediction(amount, location);
1959
+ const confidence = (Math.random() * 20 + 80).toFixed(1);
1960
+ const fraudProbability = isFraud ? (Math.random() * 30 + 70).toFixed(1) : (Math.random() * 20).toFixed(1);
1961
+ const fallbackResponse = {
1962
+ prediction: isFraud ? 'fraud' : 'safe',
1963
+ confidence: parseFloat(confidence),
1964
+ fraud_probability: parseFloat(fraudProbability)
1965
+ };
1966
+ displayPredictionResults(fallbackResponse, amount, location, merchant);
1967
+ })
1968
+ .finally(() => {
1969
+ predictButton.innerHTML = '<i class="fas fa-brain"></i> Analyze Transaction for Fraud';
1970
+ predictButton.disabled = false;
1971
+ });
1972
+
1973
+ // Normalize and adapt various API response shapes to UI-friendly values
1974
+ function normalizePredictionResponse(data) {
1975
+ // Determine prediction
1976
+ const isFraud = (
1977
+ data.fraud === 1 ||
1978
+ data.fraud_prediction === 1 ||
1979
+ data.prediction === 'fraud' ||
1980
+ data.predicted === 'fraud' ||
1981
+ data.prediction === 1
1982
+ );
1983
+
1984
+ // Determine probability (as 0..1)
1985
+ let probability = null;
1986
+ if (typeof data.probability === 'number') probability = data.probability;
1987
+ else if (typeof data.fraud_probability === 'number') probability = (data.fraud_probability > 1 ? data.fraud_probability / 100 : data.fraud_probability);
1988
+ else if (typeof data.confidence === 'number') probability = (data.confidence > 1 ? data.confidence / 100 : data.confidence);
1989
+
1990
+ // Fallback heuristic
1991
+ if (probability === null) probability = isFraud ? 0.85 : 0.12;
1992
+
1993
+ return {
1994
+ prediction: isFraud ? 'fraud' : 'safe',
1995
+ probability: Math.max(0, Math.min(1, probability)),
1996
+ confidence: data.confidence ?? Math.round(probability * 100)
1997
+ };
1998
+ }
1999
+
2000
+
2001
+ function simulateFraudPrediction(amount, location) {
2002
+ // Simple simulation logic based on dataset patterns
2003
+ let fraudScore = 0;
2004
+
2005
+ // Amount-based risk
2006
+ if (amount > 3000) fraudScore += 40;
2007
+ else if (amount > 1000) fraudScore += 20;
2008
+ else if (amount < 10) fraudScore += 15;
2009
+
2010
+ // Location-based risk (from dataset patterns)
2011
+ const highRiskLocations = ['New York', 'Dallas', 'Phoenix'];
2012
+ const mediumRiskLocations = ['San Antonio', 'Philadelphia'];
2013
+
2014
+ if (highRiskLocations.includes(location)) fraudScore += 35;
2015
+ else if (mediumRiskLocations.includes(location)) fraudScore += 20;
2016
+ else if (location === 'Luar Negeri') fraudScore += 50;
2017
+
2018
+ // Random factor
2019
+ fraudScore += Math.random() * 30;
2020
+
2021
+ return fraudScore > 60; // Threshold for fraud
2022
+ }
2023
+
2024
+ function displayPredictionResults(data, amount, location, merchant) {
2025
+ // Update UI with results (support multiple API response shapes)
2026
+ document.getElementById('resultAmount').textContent = `$${amount.toFixed(2)}`;
2027
+ document.getElementById('resultLocation').textContent = location;
2028
+ document.getElementById('resultMerchant').textContent = merchant;
2029
+
2030
+ const norm = normalizePredictionResponse(data);
2031
+
2032
+ // Update prediction badge
2033
+ predictionBadge.textContent = norm.prediction === 'fraud' ? 'FRAUD' : 'SAFE';
2034
+ predictionBadge.className = `prediction-badge ${norm.prediction === 'fraud' ? 'badge-fraud' : 'badge-safe'}`;
2035
+
2036
+ // Update probability bar: use percentage
2037
+ const pct = Math.round(norm.probability * 100);
2038
+ probabilityFill.className = `probability-fill ${norm.prediction === 'fraud' ? 'fraud-probability' : 'safe-probability'}`;
2039
+ probabilityFill.style.width = `${pct}%`;
2040
+ probabilityValue.textContent = `${pct}%`;
2041
+
2042
+ document.getElementById('resultConfidence').textContent = `${norm.confidence}%`;
2043
+
2044
+ // Show results container
2045
+ resultsContainer.style.display = 'block';
2046
+
2047
+ // Reset feedback buttons
2048
+ feedbackAccurate.classList.remove('active');
2049
+ feedbackInaccurate.classList.remove('active');
2050
+
2051
+ // Add to transaction history
2052
+ const isFraud = norm.prediction === 'fraud';
2053
+ const fraudProbability = Math.round(norm.probability * 100);
2054
+
2055
+ const newTransaction = {
2056
+ id: `TRX${10000 + transactions.length + 1}`,
2057
+ date: new Date().toISOString(),
2058
+ amount: amount,
2059
+ location: location,
2060
+ merchant: merchant,
2061
+ category: document.getElementById('transactionCategory').value,
2062
+ isFraud: isFraud,
2063
+ confidence: norm.confidence,
2064
+ status: isFraud ? 'fraud' : 'safe'
2065
+ };
2066
+
2067
+ transactions.unshift(newTransaction);
2068
+
2069
+ // Show notification
2070
+ if (isFraud) {
2071
+ showNotification('danger', `Fraud detected! Probability: ${fraudProbability}%`, true);
2072
+ } else {
2073
+ showNotification('success', `Transaction appears legitimate. Fraud probability: ${fraudProbability}%`);
2074
+ }
2075
+
2076
+ // Scroll to results
2077
+ resultsContainer.scrollIntoView({ behavior: 'smooth' });
2078
+ }
2079
+
2080
+ function submitFeedback(isAccurate) {
2081
+ // In a real app, this would send feedback to the Flask API
2082
+ // fetch(`${API_BASE_URL}/feedback`, {
2083
+ // method: 'POST',
2084
+ // headers: {
2085
+ // 'Content-Type': 'application/json',
2086
+ // },
2087
+ // body: JSON.stringify({
2088
+ // transaction_id: transactions[0].id,
2089
+ // prediction_accurate: isAccurate,
2090
+ // user_id: currentUser.id
2091
+ // })
2092
+ // })
2093
+
2094
+ showNotification('success', `Thank you for your feedback! Model accuracy will be improved.`);
2095
+
2096
+ // Reset form after feedback
2097
+ setTimeout(() => {
2098
+ predictionForm.reset();
2099
+ resultsContainer.style.display = 'none';
2100
+ }, 2000);
2101
+ }
2102
+
2103
+ function exportTransactionsToCSV() {
2104
+ // Create CSV content
2105
+ const headers = ['ID', 'Date', 'Amount', 'Location', 'Merchant', 'Category', 'Status', 'Confidence'];
2106
+ const csvContent = [
2107
+ headers.join(','),
2108
+ ...transactions.map(t => [
2109
+ t.id,
2110
+ new Date(t.date).toLocaleDateString(),
2111
+ t.amount,
2112
+ t.location,
2113
+ t.merchant,
2114
+ t.category,
2115
+ t.status.toUpperCase(),
2116
+ t.confidence
2117
+ ].join(','))
2118
+ ].join('\n');
2119
+
2120
+ // Create download link
2121
+ const blob = new Blob([csvContent], { type: 'text/csv' });
2122
+ const url = URL.createObjectURL(blob);
2123
+ const a = document.createElement('a');
2124
+ a.href = url;
2125
+ a.download = `fraud_transactions_${new Date().toISOString().split('T')[0]}.csv`;
2126
+ document.body.appendChild(a);
2127
+ a.click();
2128
+ document.body.removeChild(a);
2129
+ URL.revokeObjectURL(url);
2130
+
2131
+ showNotification('success', 'Transactions exported successfully');
2132
+ }
2133
+
2134
+ function viewTransaction(transactionId) {
2135
+ const transaction = transactions.find(t => t.id === transactionId);
2136
+ if (transaction) {
2137
+ // Switch to prediction page and populate form
2138
+ switchPage('predict');
2139
+
2140
+ // Populate form with transaction data
2141
+ document.getElementById('transactionAmount').value = transaction.amount;
2142
+ document.getElementById('transactionLocation').value = transaction.location;
2143
+ document.getElementById('merchantName').value = transaction.merchant;
2144
+ document.getElementById('transactionCategory').value = transaction.category;
2145
+
2146
+ // Trigger prediction
2147
+ setTimeout(() => {
2148
+ predictButton.click();
2149
+ }, 500);
2150
+ }
2151
+ }
2152
+
2153
+ // Notification system
2154
+ function showNotification(type, message, isImportant = false) {
2155
+ const container = document.getElementById('notificationContainer');
2156
+
2157
+ const notification = document.createElement('div');
2158
+ notification.className = `notification ${type}`;
2159
+
2160
+ const icons = {
2161
+ success: 'fa-check-circle',
2162
+ warning: 'fa-exclamation-triangle',
2163
+ danger: 'fa-times-circle',
2164
+ info: 'fa-info-circle'
2165
+ };
2166
+
2167
+ notification.innerHTML = `
2168
+ <div class="notification-icon">
2169
+ <i class="fas ${icons[type] || 'fa-info-circle'}"></i>
2170
+ </div>
2171
+ <div class="notification-content">
2172
+ <h4>${type.charAt(0).toUpperCase() + type.slice(1)}</h4>
2173
+ <p>${message}</p>
2174
+ </div>
2175
+ `;
2176
+
2177
+ container.appendChild(notification);
2178
+
2179
+ // Trigger animation
2180
+ setTimeout(() => {
2181
+ notification.classList.add('show');
2182
+ }, 10);
2183
+
2184
+ // Auto-remove notification
2185
+ setTimeout(() => {
2186
+ notification.classList.remove('show');
2187
+ setTimeout(() => {
2188
+ if (notification.parentNode) {
2189
+ notification.parentNode.removeChild(notification);
2190
+ }
2191
+ }, 500);
2192
+ }, isImportant ? 8000 : 5000);
2193
+ }
2194
+ </script>
2195
+ </body>
2196
+ </html>
frontend/icons/icon.svg ADDED
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta http-equiv="refresh" content="0; URL=fraud_detection_frontend.html" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <title>Redirecting…</title>
8
+ </head>
9
+ <body>
10
+ <p>Redirecting to <a href="fraud_detection_frontend.html">fraud_detection_frontend.html</a>.</p>
11
+ <p>If you are not redirected, click the link above.</p>
12
+ </body>
13
+ </html>
frontend/manifest.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Fraud Detection AI",
3
+ "short_name": "FraudAI",
4
+ "start_url": "/fraud_detection_frontend.html",
5
+ "display": "standalone",
6
+ "background_color": "#ffffff",
7
+ "theme_color": "#3498db",
8
+ "icons": [
9
+ {
10
+ "src": "icons/icon.svg",
11
+ "sizes": "192x192",
12
+ "type": "image/svg+xml"
13
+ }
14
+ ]
15
+ }
frontend/sw.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const CACHE_NAME = 'fraud-ai-shell-v1';
2
+ const ASSETS = [
3
+ '/',
4
+ '/fraud_detection_frontend.html',
5
+ '/manifest.json',
6
+ '/icons/icon.svg'
7
+ ];
8
+
9
+ self.addEventListener('install', (event) => {
10
+ event.waitUntil(
11
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS))
12
+ );
13
+ });
14
+
15
+ self.addEventListener('activate', (event) => {
16
+ event.waitUntil(
17
+ caches.keys().then((keys) => Promise.all(
18
+ keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))
19
+ ))
20
+ );
21
+ });
22
+
23
+ self.addEventListener('fetch', (event) => {
24
+ event.respondWith(
25
+ caches.match(event.request).then((cached) => cached || fetch(event.request))
26
+ );
27
+ });
load_tests/locustfile.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from locust import HttpUser, between, task
2
+ import random
3
+
4
+ EXAMPLE_PAYLOADS = [
5
+ {
6
+ "merchant_name":"TestA",
7
+ "avg_amount_per_transaction":123.45,
8
+ "day_of_week":2,
9
+ "amount_deviation_from_location_mean":0,
10
+ "transaction_category":"retail",
11
+ "customer_no_transactions":0,
12
+ "customer_lat":None,
13
+ "transaction_type":"online",
14
+ "customer_place_name":None,
15
+ "merchant_id":None,
16
+ "location":"New York",
17
+ "customer_job":None,
18
+ "age":None,
19
+ "merchant_long":None,
20
+ "amount_per_city_pop":0,
21
+ "customer_long":None,
22
+ "distance_customer_merchant":0,
23
+ "transactions_per_customer_ratio":0,
24
+ "customer_city_population":0,
25
+ "merchant_lat":None,
26
+ "customer_no_payments":0,
27
+ "customer_no_orders":0,
28
+ "payments_per_order_ratio":0,
29
+ "hour_of_day":12,
30
+ "amount":123.45,
31
+ "customer_zip_code":None,
32
+ "mean_amount_by_location":0,
33
+ "fraud_rate_by_location":0,
34
+ "customer_gender":None
35
+ },
36
+ ]
37
+
38
+ class PredictUser(HttpUser):
39
+ wait_time = between(1, 3)
40
+
41
+ @task(10)
42
+ def predict(self):
43
+ payload = random.choice(EXAMPLE_PAYLOADS)
44
+ self.client.post("/predict", json=payload, name="POST /predict")
45
+
46
+ @task(1)
47
+ def get_root(self):
48
+ self.client.get("/", name="GET /")
mobile/package.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "fraud-detection-mobile",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "init-capacitor": "npx cap init fraud-detection com.example.frauddetection --web-dir=www",
7
+ "add-android": "npx cap add android",
8
+ "copy": "npx cap copy",
9
+ "open-android": "npx cap open android"
10
+ },
11
+ "devDependencies": {
12
+ "@capacitor/cli": "^5.0.0",
13
+ "@capacitor/core": "^5.0.0"
14
+ }
15
+ }
models/anscombe.json ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {"Series":"I", "X":10.0, "Y":8.04},
3
+ {"Series":"I", "X":8.0, "Y":6.95},
4
+ {"Series":"I", "X":13.0, "Y":7.58},
5
+ {"Series":"I", "X":9.0, "Y":8.81},
6
+ {"Series":"I", "X":11.0, "Y":8.33},
7
+ {"Series":"I", "X":14.0, "Y":9.96},
8
+ {"Series":"I", "X":6.0, "Y":7.24},
9
+ {"Series":"I", "X":4.0, "Y":4.26},
10
+ {"Series":"I", "X":12.0, "Y":10.84},
11
+ {"Series":"I", "X":7.0, "Y":4.81},
12
+ {"Series":"I", "X":5.0, "Y":5.68},
13
+
14
+ {"Series":"II", "X":10.0, "Y":9.14},
15
+ {"Series":"II", "X":8.0, "Y":8.14},
16
+ {"Series":"II", "X":13.0, "Y":8.74},
17
+ {"Series":"II", "X":9.0, "Y":8.77},
18
+ {"Series":"II", "X":11.0, "Y":9.26},
19
+ {"Series":"II", "X":14.0, "Y":8.10},
20
+ {"Series":"II", "X":6.0, "Y":6.13},
21
+ {"Series":"II", "X":4.0, "Y":3.10},
22
+ {"Series":"II", "X":12.0, "Y":9.13},
23
+ {"Series":"II", "X":7.0, "Y":7.26},
24
+ {"Series":"II", "X":5.0, "Y":4.74},
25
+
26
+ {"Series":"III", "X":10.0, "Y":7.46},
27
+ {"Series":"III", "X":8.0, "Y":6.77},
28
+ {"Series":"III", "X":13.0, "Y":12.74},
29
+ {"Series":"III", "X":9.0, "Y":7.11},
30
+ {"Series":"III", "X":11.0, "Y":7.81},
31
+ {"Series":"III", "X":14.0, "Y":8.84},
32
+ {"Series":"III", "X":6.0, "Y":6.08},
33
+ {"Series":"III", "X":4.0, "Y":5.39},
34
+ {"Series":"III", "X":12.0, "Y":8.15},
35
+ {"Series":"III", "X":7.0, "Y":6.42},
36
+ {"Series":"III", "X":5.0, "Y":5.73},
37
+
38
+ {"Series":"IV", "X":8.0, "Y":6.58},
39
+ {"Series":"IV", "X":8.0, "Y":5.76},
40
+ {"Series":"IV", "X":8.0, "Y":7.71},
41
+ {"Series":"IV", "X":8.0, "Y":8.84},
42
+ {"Series":"IV", "X":8.0, "Y":8.47},
43
+ {"Series":"IV", "X":8.0, "Y":7.04},
44
+ {"Series":"IV", "X":8.0, "Y":5.25},
45
+ {"Series":"IV", "X":19.0, "Y":12.50},
46
+ {"Series":"IV", "X":8.0, "Y":5.56},
47
+ {"Series":"IV", "X":8.0, "Y":7.91},
48
+ {"Series":"IV", "X":8.0, "Y":6.89}
49
+ ]
models/ensemble_model.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bd836232ff34d728b60dec0f6a258b29901ed67ebfc59ea64cbaa972e84d7550
3
+ size 71633187
models/ensemble_model_enhanced.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:49d12e11259188b17aed5a911bd1ee1f1c371f785b9b8573ba88ad7d5e3540bd
3
+ size 101110099
models/preprocessor.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2bf1bcd6d2ae207b7d583789d2d93e106e072f9d08de2ab165434171aa18358a
3
+ size 3746
models/preprocessor_enhanced.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f6796d7744097da834baa4504dcdfbef995fc8e14ab1e29bf953c7ae40561709
3
+ size 49074
schemas/__init__.py ADDED
File without changes
schemas/request_schema.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # schemas/request_schema.py
2
+ from pydantic import BaseModel, Field, validator
3
+
4
+ class PredictRequest(BaseModel):
5
+ """
6
+ Schema untuk request prediksi fraud.
7
+ Menggunakan Pydantic agar validasi otomatis.
8
+ """
9
+ location: int = Field(..., description="ID lokasi transaksi")
10
+ amount: float = Field(..., description="Jumlah nominal transaksi")
11
+
12
+ @validator('amount')
13
+ def amount_must_be_positive(cls, v):
14
+ if v <= 0:
15
+ raise ValueError('amount harus > 0')
16
+ return v
17
+
18
+ @validator('location')
19
+ def location_must_be_positive(cls, v):
20
+ if v < 0:
21
+ raise ValueError('location harus >= 0')
22
+ return v
23
+
scripts/run_integration_debug.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import time
3
+ import requests
4
+ import os
5
+
6
+
7
+ def start_server(port):
8
+ env = os.environ.copy()
9
+ env["PORT"] = str(port)
10
+ print("Starting server on port", port)
11
+ proc = subprocess.Popen(
12
+ ["C:\\Users\\CINDY\\AppData\\Local\\Programs\\Python\\Python310\\python.exe", "app.py"],
13
+ stdout=subprocess.PIPE,
14
+ stderr=subprocess.PIPE,
15
+ env=env,
16
+ )
17
+ return proc
18
+
19
+
20
+ def wait_until_up(url, timeout=10.0):
21
+ start = time.time()
22
+ while True:
23
+ try:
24
+ r = requests.post(url, json={"features": [200, 1, 0, 500]}, timeout=1.0)
25
+ print("server responded", r.status_code, r.json())
26
+ return True
27
+ except Exception as e:
28
+ if time.time() - start > timeout:
29
+ print("timeout waiting for server", e)
30
+ return False
31
+ time.sleep(0.2)
32
+
33
+
34
+ def main():
35
+ port = 5002
36
+ url = f"http://127.0.0.1:{port}/predict"
37
+ proc = start_server(port)
38
+ try:
39
+ ok = wait_until_up(url)
40
+ print("wait result:", ok)
41
+ finally:
42
+ proc.terminate()
43
+ try:
44
+ proc.wait(timeout=5)
45
+ except Exception:
46
+ proc.kill()
47
+
48
+
49
+ if __name__ == '__main__':
50
+ main()
tests/test_api.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app import app
2
+
3
+ def test_predict():
4
+ client = app.test_client()
5
+
6
+ response = client.post("/predict", json={
7
+ "features": [200, 1, 0, 500]
8
+ })
9
+
10
+ assert response.status_code == 200
11
+ assert "fraud" in response.json
tests/test_integration.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import sys
3
+ import time
4
+ import requests
5
+ import os
6
+ import pytest
7
+
8
+
9
+ def start_server(port):
10
+ env = os.environ.copy()
11
+ env["PORT"] = str(port)
12
+ # Start server as a subprocess using the same Python interpreter
13
+ proc = subprocess.Popen(
14
+ [sys.executable, "app.py"],
15
+ stdout=subprocess.PIPE,
16
+ stderr=subprocess.PIPE,
17
+ env=env,
18
+ )
19
+ return proc
20
+
21
+
22
+ def wait_until_up(url, timeout=10.0):
23
+ start = time.time()
24
+ while True:
25
+ try:
26
+ requests.post(url, json={"features": [200, 1, 0, 500]}, timeout=1.0)
27
+ return True
28
+ except Exception:
29
+ if time.time() - start > timeout:
30
+ return False
31
+ time.sleep(0.2)
32
+
33
+
34
+ @pytest.mark.integration
35
+ def test_predict_integration():
36
+ port = 5001
37
+ url = f"http://127.0.0.1:{port}/predict"
38
+ proc = start_server(port)
39
+ try:
40
+ assert wait_until_up(url), "Server did not start in time"
41
+ resp = requests.post(url, json={"features": [200, 1, 0, 500]})
42
+ assert resp.status_code == 200
43
+ j = resp.json()
44
+ assert "fraud" in j
45
+ assert "probability" in j
46
+ finally:
47
+ proc.terminate()
48
+ proc.wait(timeout=5)