feat: add shipping service documentation and update seller dashboard pages and api services
This commit is contained in:
parent
de8873859e
commit
42816905c4
12 changed files with 7469 additions and 321 deletions
563
documentation/forward_integration.md
Normal file
563
documentation/forward_integration.md
Normal file
|
|
@ -0,0 +1,563 @@
|
||||||
|
# Shadowfax Unified API (SFX) — Complete Forward Integration Document
|
||||||
|
|
||||||
|
> **Document Scope**: End-to-End Technical Integration Guide for Shadowfax Logistics Platform
|
||||||
|
> **Supported Models**: Marketplace Seller Pickup & Warehouse Hub Dispatch
|
||||||
|
> **API Version**: Unified API v1 / v3 / v4
|
||||||
|
> **Document Status**: Production Ready
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Executive Summary & Architecture
|
||||||
|
|
||||||
|
Shadowfax (SFX) Unified API offers a single, standardized REST interface designed to streamline forward logistics operations across e-commerce marketplaces, brand storefronts, quick-commerce networks, and regional fulfillment hubs. The platform automates every stage of the shipment lifecycle—from pincode serviceability and pre-allocated AWB numbers to multi-location order placement, real-time shipment tracking, active escalation handling, digital proof of delivery (POD), dynamic COD QR codes, and real-time webhook callback notifications.
|
||||||
|
|
||||||
|
### Operational Models Breakdown
|
||||||
|
1. **Marketplace Model (Seller Pickup & Delivery)**:
|
||||||
|
- **First Mile**: Shadowfax delivery partners pick up shipments directly from distributed seller stores, retail outlets, or micro-warehouses.
|
||||||
|
- **Middle Mile**: Shipments are routed via local Origin Hubs and linehaul Distribution Centers (DCs).
|
||||||
|
- **Last Mile**: Delivered to the end customer address.
|
||||||
|
- **Reverse / RTS**: If delivery fails after max attempts or customer rejection, the order triggers Return to Seller (RTS) and is returned to the origin pickup location.
|
||||||
|
2. **Warehouse Model (Hub Dispatch & Delivery)**:
|
||||||
|
- **First Mile / Linehaul**: Shipments originate directly from centralized client warehouses or regional distribution hubs and are handed over to Shadowfax.
|
||||||
|
- **Last Mile**: Processed through Destination Hubs and delivered to the end customer.
|
||||||
|
- **Reverse / RTO**: If undeliverable, shipments enter Return to Origin (RTO) and return to the client warehouse.
|
||||||
|
|
||||||
|
### Environments & Host URLs
|
||||||
|
| Environment | Base URL | Purpose |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Staging / Sandbox** | `https://dale.staging.shadowfax.in` | Developer integration, QA testing & test pincodes (`110009`, `560077`, `560007`) |
|
||||||
|
| **Production** | `https://flash.shadowfax.in` | Live order processing & operational delivery management |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Technical Glossary & Core Logistics Concepts
|
||||||
|
|
||||||
|
### Facility Network Architecture
|
||||||
|
- **Distribution Center (DC)**: Large sorting facilities processing bulk intracity and intercity linehaul shipments.
|
||||||
|
- **Hub**: Neighborhood-level delivery centers managing rider assignment for last-mile delivery and first-mile seller pickup.
|
||||||
|
- **Origin Hub**: The initial Shadowfax facility receiving packages directly from sellers or micro-warehouses.
|
||||||
|
- **Origin DC**: The regional distribution center receiving packages from Origin Hubs or client primary warehouses.
|
||||||
|
- **Destination DC**: The sorting hub in the recipient city prior to local hub distribution.
|
||||||
|
- **Destination Hub**: The final delivery hub from which last-mile riders execute customer delivery.
|
||||||
|
- **Via DC**: Intermediate transit sorting hubs where manifest bags pass through without unbagging.
|
||||||
|
|
||||||
|
### Logistics Identifiers & Artifacts
|
||||||
|
- **Air Waybill (AWB)**: Unique 10-14 digit tracking identification number assigned to every parcel.
|
||||||
|
- **Letter of Receipt (LOR)**: Formal digital acknowledgement issued during physical handover of bulk parcels.
|
||||||
|
- **Proof of Delivery (POD)**: Digital evidence (recipient signature, photo timestamp, or OTP) confirming delivery.
|
||||||
|
- **COD / Cash on Delivery**: Collect-on-delivery payments collected via cash or dynamic UPI QR codes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Authentication & Global HTTP Specifications
|
||||||
|
|
||||||
|
Shadowfax API uses **HTTP Header Token Authentication**. Production and Staging API tokens are provisioned by the Shadowfax Integration Team.
|
||||||
|
|
||||||
|
### Mandatory Request Headers
|
||||||
|
```http
|
||||||
|
Authorization: Token <YOUR_PROVISIONED_API_TOKEN>
|
||||||
|
Content-Type: application/json
|
||||||
|
Accept: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standard HTTP Status Codes
|
||||||
|
| HTTP Code | Category | Meaning & Operational Handling |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `200 OK` | Success | Request succeeded. Response body contains order status or payload data. |
|
||||||
|
| `201 Created` | Success | Order created or AWB generated successfully. |
|
||||||
|
| `400 Bad Request` | Client Error | Schema validation failure or missing mandatory fields. Check field types. |
|
||||||
|
| `401 Unauthorized` | Security Error | Missing or invalid API Token in `Authorization` header. |
|
||||||
|
| `404 Not Found` | Resource Error | AWB number or Order ID does not exist in Shadowfax system. |
|
||||||
|
| `422 Unprocessable` | Business Error | Non-serviceable pincode, duplicate `client_order_id`, or invalid state transition. |
|
||||||
|
| `500 Server Error` | System Error | Downstream system unavailable; execute exponential backoff retry. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Order Lifecycle & State Transitions
|
||||||
|
|
||||||
|
### 4.1 Marketplace Order Lifecycle States
|
||||||
|
| State Name | Status Description | Next Valid States |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `new` | Order created in Shadowfax system; awaiting seller pickup assignment. | `pickup_assigned`, `cancelled` |
|
||||||
|
| `pickup_assigned` | Rider assigned for seller pickup. | `pickup_rescheduled`, `picked_up`, `pickup_cancelled` |
|
||||||
|
| `pickup_rescheduled` | Seller requested pickup deferral / pickup hold. | `pickup_assigned`, `pickup_cancelled` |
|
||||||
|
| `picked_up` | Package collected from seller; in transit to Origin Hub. | `in_transit` |
|
||||||
|
| `in_transit` | Moving between Origin Hub, DCs, and Destination Hub. | `out_for_delivery` |
|
||||||
|
| `out_for_delivery` | Package with last-mile delivery rider for customer attempt. | `delivered`, `delivery_rescheduled`, `rts_initiated` |
|
||||||
|
| `delivery_rescheduled` | Delivery attempt failed or customer requested date change. | `out_for_delivery`, `rts_initiated` |
|
||||||
|
| `delivered` | Package successfully delivered to customer. | *Terminal State* |
|
||||||
|
| `cancelled` | Order cancelled prior to pickup. | *Terminal State* |
|
||||||
|
| `rts_initiated` | Return to Seller initiated after max delivery attempts or rejection. | `rts_in_transit` |
|
||||||
|
| `rts_in_transit` | Package returning to seller location. | `rts_out_for_delivery` |
|
||||||
|
| `rts_out_for_delivery` | Rider out to hand over returned package back to seller. | `rts_delivered` |
|
||||||
|
| `rts_delivered` | Package successfully handed back to seller. | *Terminal State* |
|
||||||
|
|
||||||
|
### 4.2 Warehouse Order Lifecycle States
|
||||||
|
| State Name | Status Description | Next Valid States |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `new` | Order created for warehouse fulfillment. | `in_transit`, `cancelled` |
|
||||||
|
| `in_transit` | In transit from client warehouse to destination hub. | `out_for_delivery` |
|
||||||
|
| `out_for_delivery` | Package out for customer delivery attempt. | `delivered`, `delivery_rescheduled`, `rto_initiated` |
|
||||||
|
| `delivery_rescheduled` | Customer requested alternate delivery slot or delivery hold. | `out_for_delivery`, `rto_initiated` |
|
||||||
|
| `delivered` | Successfully delivered to customer. | *Terminal State* |
|
||||||
|
| `cancelled` | Order cancelled before dispatch. | *Terminal State* |
|
||||||
|
| `rto_initiated` | Return to Origin initiated. | `rto_in_transit` |
|
||||||
|
| `rto_in_transit` | Package moving back to client warehouse. | `rto_delivered` |
|
||||||
|
| `rto_delivered` | Package returned and verified at client warehouse. | *Terminal State* |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Push Callback API (Webhooks)
|
||||||
|
|
||||||
|
Shadowfax provides real-time event notifications via HTTP POST webhooks pushed directly to the client's configured endpoint whenever a shipment transitions state.
|
||||||
|
|
||||||
|
### Webhook Request Headers
|
||||||
|
```http
|
||||||
|
Content-Type: application/json
|
||||||
|
X-Shadowfax-Signature: <HMAC_SHA256_HASH>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Webhook Standard Payload Schema
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"client_order_id": "ORD-2026-99012",
|
||||||
|
"awb_number": "SFX10928374651",
|
||||||
|
"status": "out_for_delivery",
|
||||||
|
"sub_status": "out_for_delivery",
|
||||||
|
"remarks": "Package out for delivery with rider Rajesh (Ph: +919876543210)",
|
||||||
|
"reason_code": "",
|
||||||
|
"timestamp": "2026-09-25T14:30:00Z",
|
||||||
|
"location": {
|
||||||
|
"latitude": 12.9716,
|
||||||
|
"longitude": 77.5946,
|
||||||
|
"city": "Bengaluru"
|
||||||
|
},
|
||||||
|
"attempt_count": 1,
|
||||||
|
"pickup_details": {
|
||||||
|
"pickup_id": "PICKUP-8812",
|
||||||
|
"name": "Acme Electronics Hub"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Webhook Event State Mappings
|
||||||
|
| Webhook `status` | Marketplace State Equivalent | Warehouse State Equivalent | Trigger Event |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `pickup_assigned` | `pickup_assigned` | N/A | Pickup executive assigned to seller. |
|
||||||
|
| `picked_up` | `picked_up` | N/A | Package scanned at seller location. |
|
||||||
|
| `in_transit` | `in_transit` | `in_transit` | Package arrived or departed DC/Hub. |
|
||||||
|
| `out_for_delivery` | `out_for_delivery` | `out_for_delivery` | Package scanned onto rider runsheet. |
|
||||||
|
| `delivered` | `delivered` | `delivered` | Successful delivery to customer. |
|
||||||
|
| `delivery_rescheduled`| `delivery_rescheduled`| `delivery_rescheduled`| Delivery attempt deferred with reason code. |
|
||||||
|
| `rts_initiated` | `rts_initiated` | N/A | Return to seller triggered. |
|
||||||
|
| `rto_initiated` | N/A | `rto_initiated` | Return to origin warehouse triggered. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Endpoints Reference & Full API Specifications
|
||||||
|
|
||||||
|
### 6.1 Serviceability API
|
||||||
|
**HTTP Method**: `GET`
|
||||||
|
**Endpoint Path**: `/v1/clients/serviceability/?service={service}&page={page}&count={count}&pincodes={pincodes}`
|
||||||
|
**Description**: Fetches list of serviceable pincodes or verifies if a specific destination pincode is active for delivery/pickup.
|
||||||
|
|
||||||
|
#### Query Parameters
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `service` | String | No | Service filter (e.g. `express`, `surface`, `reverse`). |
|
||||||
|
| `pincodes` | String | No | Comma-separated list of 6-digit Indian pincodes to query. |
|
||||||
|
| `page` | Integer | No | Page number for paginated pincode index (Default: `1`). |
|
||||||
|
| `count` | Integer | No | Number of records per page (Default: `100`, Max: `1000`). |
|
||||||
|
|
||||||
|
#### Sample Response Payload (`200 OK`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Serviceability retrieved successfully",
|
||||||
|
"data": {
|
||||||
|
"total_count": 2,
|
||||||
|
"page": 1,
|
||||||
|
"pincodes": [
|
||||||
|
{
|
||||||
|
"pincode": "560077",
|
||||||
|
"city": "Bengaluru",
|
||||||
|
"state": "Karnataka",
|
||||||
|
"serviceable": true,
|
||||||
|
"pickup_available": true,
|
||||||
|
"delivery_available": true,
|
||||||
|
"cod_available": true,
|
||||||
|
"prepaid_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pincode": "110009",
|
||||||
|
"city": "New Delhi",
|
||||||
|
"state": "Delhi",
|
||||||
|
"serviceable": true,
|
||||||
|
"pickup_available": true,
|
||||||
|
"delivery_available": true,
|
||||||
|
"cod_available": true,
|
||||||
|
"prepaid_available": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 AWB Generation API
|
||||||
|
**HTTP Method**: `POST`
|
||||||
|
**Endpoint Path**: `/v3/clients/generate_marketplace_awb/`
|
||||||
|
**Description**: Pre-allocates a Shadowfax AWB tracking number prior to creating the complete order payload.
|
||||||
|
|
||||||
|
#### Sample Request Payload
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"client_code": "ACME_RETAIL",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sample Response Payload (`200 OK`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"awb_numbers": [
|
||||||
|
"SFX9928374102"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Marketplace Order Creation API (Seller Pickup Model)
|
||||||
|
**HTTP Method**: `POST`
|
||||||
|
**Endpoint Path**: `/v3/clients/orders/`
|
||||||
|
**Description**: Creates a seller pickup request and customer delivery order in a single API call.
|
||||||
|
|
||||||
|
#### Request Payload Parameter Matrix
|
||||||
|
##### 1. `order_details` Object
|
||||||
|
| Field Name | Type | Required | Validation & Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `client_order_id` | String | **Yes** | Client internal order ID (Must be unique across system). |
|
||||||
|
| `awb_number` | String | No | Pre-generated Shadowfax AWB number. Auto-generated if omitted. |
|
||||||
|
| `actual_weight` | Number | **Yes** | Weight of shipment in grams (e.g. `500.0`). |
|
||||||
|
| `volumetric_weight` | Number | No | Volumetric weight in grams `(L * W * H) / 5000` (Default: `0.0`). |
|
||||||
|
| `product_value` | Number | **Yes** | Total invoice product value in INR excluding tax. |
|
||||||
|
| `payment_mode` | String | **Yes** | Payment type: `"Prepaid"` or `"COD"`. |
|
||||||
|
| `cod_amount` | Number | **Yes** | COD amount to collect from recipient (`0` if `Prepaid`). |
|
||||||
|
| `promised_delivery_date` | String | No | Expected delivery date format `YYYY-MM-DD`. |
|
||||||
|
|
||||||
|
##### 2. `customer_details` Object
|
||||||
|
| Field Name | Type | Required | Validation & Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `name` | String | **Yes** | Recipient full name. |
|
||||||
|
| `phone` | String | **Yes** | 10-digit mobile number of recipient. |
|
||||||
|
| `address_line_1` | String | **Yes** | Building, street, flat number. |
|
||||||
|
| `address_line_2` | String | No | Landmark, locality. |
|
||||||
|
| `pincode` | String | **Yes** | 6-digit serviceable delivery pincode. |
|
||||||
|
| `city` | String | **Yes** | Destination city. |
|
||||||
|
| `state` | String | **Yes** | Destination state. |
|
||||||
|
|
||||||
|
##### 3. `pickup_details` Object
|
||||||
|
| Field Name | Type | Required | Validation & Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `name` | String | **Yes** | Seller / Store / Warehouse name. |
|
||||||
|
| `phone` | String | **Yes** | 10-digit contact mobile number of seller. |
|
||||||
|
| `address_line_1` | String | **Yes** | Seller warehouse address. |
|
||||||
|
| `pincode` | String | **Yes** | 6-digit origin pickup pincode. |
|
||||||
|
| `city` | String | **Yes** | Origin city. |
|
||||||
|
| `state` | String | **Yes** | Origin state. |
|
||||||
|
|
||||||
|
##### 4. `rts_details` Object (Return to Seller)
|
||||||
|
| Field Name | Type | Required | Validation & Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `name` | String | **Yes** | Return contact name. |
|
||||||
|
| `phone` | String | **Yes** | Return contact phone number. |
|
||||||
|
| `address_line_1` | String | **Yes** | Return seller address. |
|
||||||
|
| `pincode` | String | **Yes** | 6-digit return pincode. |
|
||||||
|
|
||||||
|
##### 5. `product_details` Array Items
|
||||||
|
| Field Name | Type | Required | Validation & Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `sku_name` | String | **Yes** | Item title or product name. |
|
||||||
|
| `sku_code` | String | **Yes** | Merchant SKU code. |
|
||||||
|
| `price` | Number | **Yes** | Individual SKU price. |
|
||||||
|
| `quantity` | Integer | **Yes** | Quantity of items. |
|
||||||
|
|
||||||
|
#### Complete Marketplace Request Payload Example
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"order_type": "marketplace",
|
||||||
|
"order_details": {
|
||||||
|
"client_order_id": "ORD-MKT-2026-101",
|
||||||
|
"awb_number": "SFX9928374102",
|
||||||
|
"actual_weight": 750.0,
|
||||||
|
"volumetric_weight": 800.0,
|
||||||
|
"product_value": 1499.0,
|
||||||
|
"payment_mode": "COD",
|
||||||
|
"cod_amount": 1499.0
|
||||||
|
},
|
||||||
|
"customer_details": {
|
||||||
|
"name": "Ananya Sharma",
|
||||||
|
"phone": "9876543210",
|
||||||
|
"address_line_1": "Flat 402, Sunshine Apartments",
|
||||||
|
"address_line_2": "Indiranagar 10th Main",
|
||||||
|
"pincode": "560038",
|
||||||
|
"city": "Bengaluru",
|
||||||
|
"state": "Karnataka"
|
||||||
|
},
|
||||||
|
"pickup_details": {
|
||||||
|
"name": "StyleHub Retail Store",
|
||||||
|
"phone": "9123456789",
|
||||||
|
"address_line_1": "Plot 12, Industrial Area Phase 2",
|
||||||
|
"pincode": "560077",
|
||||||
|
"city": "Bengaluru",
|
||||||
|
"state": "Karnataka"
|
||||||
|
},
|
||||||
|
"rts_details": {
|
||||||
|
"name": "StyleHub Returns Center",
|
||||||
|
"phone": "9123456789",
|
||||||
|
"address_line_1": "Plot 12, Industrial Area Phase 2",
|
||||||
|
"pincode": "560077",
|
||||||
|
"city": "Bengaluru",
|
||||||
|
"state": "Karnataka"
|
||||||
|
},
|
||||||
|
"product_details": [
|
||||||
|
{
|
||||||
|
"sku_name": "Wireless Noise Cancelling Earbuds",
|
||||||
|
"sku_code": "SKU-EAR-001",
|
||||||
|
"price": 1499.0,
|
||||||
|
"quantity": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sample Response Payload (`201 Created`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Order created successfully",
|
||||||
|
"data": {
|
||||||
|
"client_order_id": "ORD-MKT-2026-101",
|
||||||
|
"awb_number": "SFX9928374102",
|
||||||
|
"order_status": "new",
|
||||||
|
"created_at": "2026-09-25T14:35:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 Warehouse Order Creation API (Hub Dispatch Model)
|
||||||
|
**HTTP Method**: `POST`
|
||||||
|
**Endpoint Path**: `/v3/clients/orders/`
|
||||||
|
**Description**: Creates a delivery order for shipments dispatched directly from a client warehouse or hub.
|
||||||
|
|
||||||
|
#### Complete Warehouse Request Payload Example
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"order_type": "warehouse",
|
||||||
|
"order_details": {
|
||||||
|
"client_order_id": "ORD-WH-2026-505",
|
||||||
|
"awb_number": "SFX8817263541",
|
||||||
|
"actual_weight": 1200.0,
|
||||||
|
"product_value": 3500.0,
|
||||||
|
"payment_mode": "Prepaid",
|
||||||
|
"cod_amount": 0.0
|
||||||
|
},
|
||||||
|
"customer_details": {
|
||||||
|
"name": "Vikram Mehta",
|
||||||
|
"phone": "9988776655",
|
||||||
|
"address_line_1": "House 14, Civil Lines",
|
||||||
|
"pincode": "110054",
|
||||||
|
"city": "New Delhi",
|
||||||
|
"state": "Delhi"
|
||||||
|
},
|
||||||
|
"rto_details": {
|
||||||
|
"name": "Central Warehouse North",
|
||||||
|
"phone": "9811002233",
|
||||||
|
"address_line_1": "Warehouse Complex Hub 3",
|
||||||
|
"pincode": "110042",
|
||||||
|
"city": "New Delhi",
|
||||||
|
"state": "Delhi"
|
||||||
|
},
|
||||||
|
"product_details": [
|
||||||
|
{
|
||||||
|
"sku_name": "Mechanical Keyboard RGB",
|
||||||
|
"sku_code": "SKU-KB-102",
|
||||||
|
"price": 3500.0,
|
||||||
|
"quantity": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.5 Single Order Tracking API (V4)
|
||||||
|
**HTTP Method**: `GET`
|
||||||
|
**Endpoint Path**: `/v4/clients/orders/{awb_number}/track/`
|
||||||
|
**Description**: Retrieves comprehensive tracking timeline, current location, rider details, and status for a shipment.
|
||||||
|
|
||||||
|
#### Sample Response Payload (`200 OK`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"client_order_id": "ORD-MKT-2026-101",
|
||||||
|
"awb_number": "SFX9928374102",
|
||||||
|
"current_status": "out_for_delivery",
|
||||||
|
"expected_delivery_date": "2026-09-25",
|
||||||
|
"rider_details": {
|
||||||
|
"name": "Rajesh Kumar",
|
||||||
|
"phone": "+919876543210"
|
||||||
|
},
|
||||||
|
"tracking_history": [
|
||||||
|
{
|
||||||
|
"status": "new",
|
||||||
|
"location": "Bengaluru Hub",
|
||||||
|
"timestamp": "2026-09-24T10:00:00Z",
|
||||||
|
"remarks": "Order created"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "picked_up",
|
||||||
|
"location": "StyleHub Store",
|
||||||
|
"timestamp": "2026-09-24T14:30:00Z",
|
||||||
|
"remarks": "Package picked up from seller"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "in_transit",
|
||||||
|
"location": "Bengaluru Main DC",
|
||||||
|
"timestamp": "2026-09-24T19:00:00Z",
|
||||||
|
"remarks": "Arrived at sorting DC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "out_for_delivery",
|
||||||
|
"location": "Indiranagar Hub",
|
||||||
|
"timestamp": "2026-09-25T08:30:00Z",
|
||||||
|
"remarks": "Assigned to rider for delivery"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.6 Bulk Order Tracking API (V4)
|
||||||
|
**HTTP Method**: `POST`
|
||||||
|
**Endpoint Path**: `/v4/clients/bulk_track/`
|
||||||
|
**Description**: Fetches current tracking status for up to 50 AWB numbers in a single request.
|
||||||
|
|
||||||
|
#### Sample Request Payload
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"awb_numbers": [
|
||||||
|
"SFX9928374102",
|
||||||
|
"SFX8817263541"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.7 Order Cancellation API
|
||||||
|
**HTTP Method**: `POST`
|
||||||
|
**Endpoint Path**: `/v3/clients/orders/cancel/`
|
||||||
|
**Description**: Cancels a pending order prior to rider pickup or warehouse dispatch.
|
||||||
|
|
||||||
|
#### Sample Request Payload
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"client_order_id": "ORD-MKT-2026-101",
|
||||||
|
"awb_number": "SFX9928374102",
|
||||||
|
"reason": "Customer requested cancellation"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.8 Escalation & Support Ticket API
|
||||||
|
**HTTP Method**: `POST`
|
||||||
|
**Endpoint Path**: `/v1/clients/support/issue/`
|
||||||
|
**Description**: Raises operational support tickets directly to Shadowfax ground control team.
|
||||||
|
|
||||||
|
#### Issue Categories Available
|
||||||
|
- `DELAYED_DELIVERY`: Delivery SLA breached or delayed.
|
||||||
|
- `EXPEDITE_PICKUP`: Urgent seller pickup required.
|
||||||
|
- `DELIVERY_DISPUTE`: Customer disputes status marked delivered.
|
||||||
|
- `ADDRESS_UPDATE`: Correcting destination address/phone.
|
||||||
|
|
||||||
|
#### Sample Request Payload
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"awb_number": "SFX9928374102",
|
||||||
|
"category": "EXPEDITE_PICKUP",
|
||||||
|
"description": "Seller store closing early today at 4 PM, please pick up by 3 PM."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.9 Proof of Delivery (POD) API
|
||||||
|
**HTTP Method**: `POST`
|
||||||
|
**Endpoint Path**: `/v1/clients/pod_details/`
|
||||||
|
**Description**: Retrieves digital signature image URL, photo proof, or OTP verification details for delivered shipments.
|
||||||
|
|
||||||
|
#### Sample Response Payload (`200 OK`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"awb_number": "SFX9928374102",
|
||||||
|
"delivery_timestamp": "2026-09-25T11:45:00Z",
|
||||||
|
"signature_url": "https://cdn.shadowfax.in/pod/signatures/SFX9928374102.png",
|
||||||
|
"photo_proof_url": "https://cdn.shadowfax.in/pod/photos/SFX9928374102.jpg",
|
||||||
|
"receiver_name": "Ananya Sharma",
|
||||||
|
"otp_verified": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.10 Dynamic QR Code Generation API (V2)
|
||||||
|
**HTTP Method**: `POST`
|
||||||
|
**Endpoint Path**: `/v2/clients/generate_qr_code/`
|
||||||
|
**Description**: Generates dynamic UPI payment QR code strings for Cash-on-Delivery collection at customer doorstep.
|
||||||
|
|
||||||
|
#### Sample Response Payload (`200 OK`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"awb_number": "SFX9928374102",
|
||||||
|
"cod_amount": 1499.0,
|
||||||
|
"qr_code_string": "upi://pay?pa=shadowfax@icici&pn=Shadowfax&am=1499.00&tr=SFX9928374102&tn=COD%20Payment",
|
||||||
|
"qr_code_image_base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Appendix: Standard Reason & Remark Codes Catalog
|
||||||
|
|
||||||
|
Shadowfax uses standardized numerical and string remark codes during exception events. Client system webhooks should parse these codes for automated business logic.
|
||||||
|
|
||||||
|
### 7.1 Delivery On-Hold Remark Codes
|
||||||
|
| Reason Code | Code Description | Suggested Operational Handling |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `OH_ADDR_INCOMPLETE` | Customer address incomplete or missing landmark. | Trigger SMS/WhatsApp to customer to update address. |
|
||||||
|
| `OH_CUST_UNAVAILABLE` | Customer not available at address. | Reschedule for next business day automatically. |
|
||||||
|
| `OH_COD_NOT_READY` | Customer requested delivery when cash/UPI ready. | Reschedule delivery date per customer request. |
|
||||||
|
| `OH_PINCODE_UNMAPPED` | Destination pincode out of delivery hub zone. | Trigger escalation ticket to reassign hub. |
|
||||||
|
|
||||||
|
### 7.2 Pickup On-Hold Remark Codes
|
||||||
|
| Reason Code | Code Description | Suggested Operational Handling |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `PU_STORE_CLOSED` | Seller store/warehouse closed during pickup attempt. | Notify seller contact via alert. |
|
||||||
|
| `PU_ITEM_NOT_READY` | Shipment packaging incomplete at seller end. | Reschedule seller pickup window. |
|
||||||
|
| `PU_ADDR_NOT_FOUND` | Seller pickup address untraceable. | Verify pickup coordinates and phone number. |
|
||||||
|
|
||||||
|
### 7.3 Cancellation Reason Codes
|
||||||
|
| Reason Code | Code Description | Originator |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `CAN_CLIENT_REQ` | Order cancelled by client system via API. | Client |
|
||||||
|
| `CAN_SELLER_DENIED` | Seller denied stock availability during pickup. | Seller |
|
||||||
|
| `CAN_EXCEEDED_ATTEMPTS`| Max delivery attempts reached (RTS/RTO triggered).| Courier |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Summary & Next Steps for Integration
|
||||||
|
|
||||||
|
1. **Credential Setup**: Obtain Staging API Token and test pincode list from Shadowfax Integration Team.
|
||||||
|
2. **Webhook Endpoint Registration**: Configure HTTPS POST Webhook listener and verify HMAC signature parsing.
|
||||||
|
3. **End-to-End Sandbox Testing**: Execute Pincode Check $\rightarrow$ Order Creation $\rightarrow$ Tracking $\rightarrow$ Webhook Receive $\rightarrow$ POD retrieval flow.
|
||||||
|
4. **Go-Live Readiness Review**: Obtain sign-off from Shadowfax Logistics Operations for Production URL switch.
|
||||||
418
documentation/reverse_integration.md
Normal file
418
documentation/reverse_integration.md
Normal file
|
|
@ -0,0 +1,418 @@
|
||||||
|
# Shadowfax Reverse Pickup API (SFX) — Complete Reverse Integration Document
|
||||||
|
|
||||||
|
> **Document Scope**: Technical Specification & Integration Guide for Shadowfax Reverse Logistics & Customer Return Platform
|
||||||
|
> **Core Capability**: Doorstep Pickups, Quality Check (QC) Enforcement, Warehouse Returns & Callback Synchronization
|
||||||
|
> **API Version**: Reverse API v1 / v2 / v3 / v4
|
||||||
|
> **Document Status**: Production Ready
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. System Architecture & Reverse Logistics Overview
|
||||||
|
|
||||||
|
The Shadowfax (SFX) Reverse Pickup API enables e-commerce merchants, marketplace operators, and brand retailers to automate customer return pickups and doorstep Quality Checks (QC). Unlike forward delivery where parcels originate from seller/warehouse locations, reverse logistics manages customer doorstep pickups, item inspection, hub consolidation, linehaul transport, and final delivery back to the client's return warehouse.
|
||||||
|
|
||||||
|
### Key Operational Capabilities
|
||||||
|
1. **Doorstep Customer Pickup**: Delivery executive collects items directly from the customer's specified pickup address.
|
||||||
|
2. **Configurable Quality Check (QC)**: Executive verifies physical item condition, brand, serial/IMEI numbers, original packaging, and tags prior to accept/reject.
|
||||||
|
3. **Reverse Linehaul & Consolidation**: Collected items are routed through Shadowfax Origin Hubs and linehaul Distribution Centers (DCs).
|
||||||
|
4. **Warehouse Return Delivery**: Final delivery and digital handover of collected return inventory to client fulfillment centers.
|
||||||
|
5. **Real-time Callback Synchronization**: Push webhooks notify client systems instantly upon pickup status change, QC pass/fail events, and warehouse delivery.
|
||||||
|
|
||||||
|
### Environment Base URLs
|
||||||
|
| Environment | Base URL | Purpose |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Staging / Sandbox** | `https://dale.staging.shadowfax.in` | Integration testing & test pincodes (`110009`, `560077`, `560007`) |
|
||||||
|
| **Production** | `https://flash.shadowfax.in` | Live reverse order creation & operational execution |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Technical Glossary & Reverse Network Facilities
|
||||||
|
|
||||||
|
### Facility Network Architecture
|
||||||
|
- **Pickup Customer Location**: The customer residential or commercial address where the return item is collected.
|
||||||
|
- **Origin Hub**: First Shadowfax local hub receiving the picked-up return item from the delivery rider.
|
||||||
|
- **Origin DC**: Primary sorting Distribution Center processing reverse linehaul routing.
|
||||||
|
- **Destination DC**: Sorting center located near the client return warehouse.
|
||||||
|
- **Destination Hub / Return Hub**: Final Shadowfax hub executing bulk delivery back to the client warehouse.
|
||||||
|
- **Client Return Warehouse**: Final destination facility where returned goods are handed over to the merchant.
|
||||||
|
|
||||||
|
### Reverse Logistics Identifiers
|
||||||
|
- **Client Request ID / Order ID**: Client's unique internal reference for the return request.
|
||||||
|
- **Reverse Air Waybill (AWB)**: Unique tracking number assigned to the reverse shipment.
|
||||||
|
- **QC Parameter**: Inspection rules (e.g., brand check, IMEI match, unused status) defined per item.
|
||||||
|
- **Proof of Pickup (POP) / POD**: Digital signature or photo timestamp confirming return item collection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Authentication & Global HTTP Specifications
|
||||||
|
|
||||||
|
Shadowfax API uses **HTTP Header Token Authentication**. Production and Staging API tokens are provisioned by the Shadowfax Integration Team.
|
||||||
|
|
||||||
|
### Mandatory HTTP Headers
|
||||||
|
```http
|
||||||
|
Authorization: Token <YOUR_PROVISIONED_API_TOKEN>
|
||||||
|
Content-Type: application/json
|
||||||
|
Accept: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standard HTTP Status Codes
|
||||||
|
| HTTP Code | Category | Operational Meaning & Action |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `200 OK` | Success | Request succeeded. Body contains request details or query results. |
|
||||||
|
| `201 Created` | Success | Reverse pickup request successfully created. |
|
||||||
|
| `400 Bad Request` | Validation Error | Missing mandatory field or invalid data type format. |
|
||||||
|
| `401 Unauthorized` | Security Error | Invalid or missing API token in `Authorization` header. |
|
||||||
|
| `404 Not Found` | Resource Error | Client Request ID or Reverse AWB number does not exist. |
|
||||||
|
| `422 Unprocessable` | Business Rule | Non-serviceable pickup pincode or duplicate `client_request_id`. |
|
||||||
|
| `500 Server Error` | System Exception | Downstream system unavailable; execute retry with exponential backoff. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Reverse Order Lifecycle & Quality Check (QC) Architecture
|
||||||
|
|
||||||
|
### 4.1 Reverse Pickup Lifecycle States
|
||||||
|
| State Name | Status Description | Next Valid States |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `new` | Pickup request created in Shadowfax system; awaiting rider assignment. | `pickup_assigned`, `cancelled` |
|
||||||
|
| `pickup_assigned` | Rider assigned to visit customer address. | `picked_up`, `qc_failed`, `pickup_rescheduled`, `cancelled` |
|
||||||
|
| `pickup_rescheduled` | Customer requested alternate pickup date/time window. | `pickup_assigned`, `cancelled` |
|
||||||
|
| `picked_up` | Item passed QC checks and collected from customer; in transit to Origin Hub. | `in_transit` |
|
||||||
|
| `qc_failed` | Item failed doorstep QC checks (e.g. wrong brand, damaged item). Pickup rejected. | *Terminal State* |
|
||||||
|
| `in_transit` | Item moving between Shadowfax Hubs and linehaul DCs toward client warehouse. | `out_for_delivery_to_warehouse` |
|
||||||
|
| `out_for_delivery_to_warehouse` | Item assigned to rider for bulk handover at client return warehouse. | `delivered_to_warehouse`, `delivery_rescheduled` |
|
||||||
|
| `delivered_to_warehouse` | Return item verified and received at client warehouse. | *Terminal State* |
|
||||||
|
| `cancelled` | Reverse pickup request cancelled prior to pickup execution. | *Terminal State* |
|
||||||
|
|
||||||
|
### 4.2 Quality Check (QC) Enforcement Rules
|
||||||
|
Shadowfax riders perform doorstep QC based on criteria supplied in the order payload:
|
||||||
|
1. **Brand & Model Match**: Verifies item brand name and model match order manifest.
|
||||||
|
2. **Serial / IMEI Verification**: Matches 15-digit mobile IMEI or product serial number.
|
||||||
|
3. **Physical Damage Inspection**: Checks for cracks, dents, water damage, or missing components.
|
||||||
|
4. **Tag / Packaging Verification**: Confirms original brand tags/box are intact if required.
|
||||||
|
5. **Doorstep Image Capture**: Rider uploads photo proof of item condition during pickup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Push Callback API (Webhooks)
|
||||||
|
|
||||||
|
Shadowfax notifies client systems in real time whenever a reverse pickup transitions state.
|
||||||
|
|
||||||
|
### Webhook Request Headers
|
||||||
|
```http
|
||||||
|
Content-Type: application/json
|
||||||
|
X-Shadowfax-Signature: <HMAC_SHA256_HASH>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Webhook Standard Payload Schema
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"client_request_id": "REV-ORD-2026-8801",
|
||||||
|
"awb_number": "SFX-REV-99182736",
|
||||||
|
"status": "picked_up",
|
||||||
|
"sub_status": "qc_passed",
|
||||||
|
"remarks": "Item passed physical QC and picked up by rider Ramesh",
|
||||||
|
"reason_code": "",
|
||||||
|
"qc_result": {
|
||||||
|
"qc_passed": true,
|
||||||
|
"brand_matched": true,
|
||||||
|
"imei_matched": true,
|
||||||
|
"serial_number": "SN-9988112233",
|
||||||
|
"images": [
|
||||||
|
"https://cdn.shadowfax.in/qc/photos/REV-ORD-2026-8801_1.jpg"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timestamp": "2026-09-25T15:10:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. API Endpoints Reference & Complete Technical Specifications
|
||||||
|
|
||||||
|
### 6.1 Reverse Pincode Serviceability API
|
||||||
|
**HTTP Method**: `GET`
|
||||||
|
**Endpoint Path**: `/v1/clients/serviceability/?service=reverse&page={page}&count={count}&pincodes={pincodes}`
|
||||||
|
**Description**: Verifies if customer pickup pincodes are active for reverse pickup operations.
|
||||||
|
|
||||||
|
#### Query Parameters
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `service` | String | **Yes** | Pass `"reverse"` for reverse pickup serviceability query. |
|
||||||
|
| `pincodes` | String | No | Comma-separated 6-digit Indian pincodes to check. |
|
||||||
|
| `page` | Integer | No | Page number for paginated results (Default: `1`). |
|
||||||
|
| `count` | Integer | No | Records per page (Default: `100`). |
|
||||||
|
|
||||||
|
#### Sample Response Payload (`200 OK`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"total_count": 1,
|
||||||
|
"pincodes": [
|
||||||
|
{
|
||||||
|
"pincode": "560077",
|
||||||
|
"city": "Bengaluru",
|
||||||
|
"state": "Karnataka",
|
||||||
|
"reverse_pickup_available": true,
|
||||||
|
"max_weight_grams": 5000.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Reverse AWB Generation API
|
||||||
|
**HTTP Method**: `POST`
|
||||||
|
**Endpoint Path**: `/v3/clients/orders/generate_awb/`
|
||||||
|
**Description**: Pre-allocates tracking numbers for reverse pickup shipments.
|
||||||
|
|
||||||
|
#### Sample Request Payload
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"client_code": "ACME_RETAIL",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sample Response Payload (`200 OK`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"awb_numbers": [
|
||||||
|
"SFX-REV-99182736"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Reverse Pickup Request Creation API
|
||||||
|
**HTTP Method**: `POST`
|
||||||
|
**Endpoint Path**: `/v3/clients/requests`
|
||||||
|
**Description**: Creates a customer doorstep pickup request with embedded QC parameters.
|
||||||
|
|
||||||
|
#### Request Payload Parameter Matrix
|
||||||
|
##### 1. Root Level & Order Identification
|
||||||
|
| Field Name | Type | Required | Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `client_request_id` | String | **Yes** | Client internal unique return request identifier. |
|
||||||
|
| `awb_number` | String | No | Pre-allocated reverse AWB number. Auto-generated if omitted. |
|
||||||
|
| `actual_weight` | Number | **Yes** | Expected weight of return item in grams. |
|
||||||
|
| `product_value` | Number | **Yes** | Invoice price of item being returned in INR. |
|
||||||
|
| `pickup_date` | String | No | Scheduled pickup date format `YYYY-MM-DD`. |
|
||||||
|
|
||||||
|
##### 2. `pickup_details` (Customer Location)
|
||||||
|
| Field Name | Type | Required | Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `name` | String | **Yes** | Customer full name. |
|
||||||
|
| `phone` | String | **Yes** | 10-digit mobile number of customer. |
|
||||||
|
| `address_line_1` | String | **Yes** | Customer street address and flat/house number. |
|
||||||
|
| `pincode` | String | **Yes** | 6-digit serviceable pickup pincode. |
|
||||||
|
| `city` | String | **Yes** | Pickup city. |
|
||||||
|
| `state` | String | **Yes** | Pickup state. |
|
||||||
|
|
||||||
|
##### 3. `return_details` (Client Warehouse Destination)
|
||||||
|
| Field Name | Type | Required | Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `name` | String | **Yes** | Warehouse return hub name. |
|
||||||
|
| `phone` | String | **Yes** | Contact phone number of return warehouse. |
|
||||||
|
| `address_line_1` | String | **Yes** | Warehouse street address. |
|
||||||
|
| `pincode` | String | **Yes** | 6-digit warehouse pincode. |
|
||||||
|
| `city` | String | **Yes** | Warehouse city. |
|
||||||
|
| `state` | String | **Yes** | Warehouse state. |
|
||||||
|
|
||||||
|
##### 4. `qc_details` Object (Quality Check Instructions)
|
||||||
|
| Field Name | Type | Required | Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| `qc_check` | Boolean | **Yes** | `true` to enforce doorstep QC inspection. |
|
||||||
|
| `brand_name` | String | No | Expected brand name on item label. |
|
||||||
|
| `item_name` | String | **Yes** | SKU name / description. |
|
||||||
|
| `serial_number` | String | No | Expected product serial or IMEI number. |
|
||||||
|
| `image_required` | Boolean | No | `true` if rider must take photo proof during QC. |
|
||||||
|
|
||||||
|
#### Complete Reverse Pickup Request Payload Example
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"client_request_id": "REV-ORD-2026-8801",
|
||||||
|
"awb_number": "SFX-REV-99182736",
|
||||||
|
"actual_weight": 500.0,
|
||||||
|
"product_value": 2999.0,
|
||||||
|
"pickup_details": {
|
||||||
|
"name": "Kavita Reddy",
|
||||||
|
"phone": "9876501234",
|
||||||
|
"address_line_1": "Apartment 102, Green Glen Layout",
|
||||||
|
"address_line_2": "Bellandur",
|
||||||
|
"pincode": "560103",
|
||||||
|
"city": "Bengaluru",
|
||||||
|
"state": "Karnataka"
|
||||||
|
},
|
||||||
|
"return_details": {
|
||||||
|
"name": "Acme Central Returns Warehouse",
|
||||||
|
"phone": "9811004455",
|
||||||
|
"address_line_1": "Logistics Park Warehouse 4B",
|
||||||
|
"pincode": "560077",
|
||||||
|
"city": "Bengaluru",
|
||||||
|
"state": "Karnataka"
|
||||||
|
},
|
||||||
|
"qc_details": {
|
||||||
|
"qc_check": true,
|
||||||
|
"brand_name": "SoundCore",
|
||||||
|
"item_name": "Over-Ear Headphones Black",
|
||||||
|
"serial_number": "SN-9988112233",
|
||||||
|
"image_required": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sample Response Payload (`201 Created`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Reverse pickup request created successfully",
|
||||||
|
"data": {
|
||||||
|
"client_request_id": "REV-ORD-2026-8801",
|
||||||
|
"awb_number": "SFX-REV-99182736",
|
||||||
|
"status": "new",
|
||||||
|
"created_at": "2026-09-25T15:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 Reverse Order Cancellation API
|
||||||
|
**HTTP Method**: `POST`
|
||||||
|
**Endpoint Path**: `/v2/clients/requests/mark_cancel`
|
||||||
|
**Description**: Cancels a pending customer pickup request before rider execution.
|
||||||
|
|
||||||
|
#### Sample Request Payload
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"client_request_id": "REV-ORD-2026-8801",
|
||||||
|
"reason": "Customer withdrew return request"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.5 Single Pickup Request Tracking API (V4)
|
||||||
|
**HTTP Method**: `GET`
|
||||||
|
**Endpoint Path**: `/v4/clients/requests/{client_request_id}`
|
||||||
|
**Description**: Retrieves tracking timeline, rider contact, QC status, and location for a reverse pickup.
|
||||||
|
|
||||||
|
#### Sample Response Payload (`200 OK`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"client_request_id": "REV-ORD-2026-8801",
|
||||||
|
"awb_number": "SFX-REV-99182736",
|
||||||
|
"current_status": "picked_up",
|
||||||
|
"qc_status": "PASSED",
|
||||||
|
"rider_details": {
|
||||||
|
"name": "Ramesh Kumar",
|
||||||
|
"phone": "+919876543210"
|
||||||
|
},
|
||||||
|
"tracking_history": [
|
||||||
|
{
|
||||||
|
"status": "new",
|
||||||
|
"timestamp": "2026-09-25T15:00:00Z",
|
||||||
|
"remarks": "Reverse pickup requested"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "pickup_assigned",
|
||||||
|
"timestamp": "2026-09-25T15:15:00Z",
|
||||||
|
"remarks": "Assigned to rider Ramesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "picked_up",
|
||||||
|
"timestamp": "2026-09-25T16:00:00Z",
|
||||||
|
"remarks": "Item passed QC check and collected from customer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.6 Bulk Pickup Request Tracking API (V4)
|
||||||
|
**HTTP Method**: `POST`
|
||||||
|
**Endpoint Path**: `/v4/clients/requests/bulk_query`
|
||||||
|
**Description**: Batch queries status for up to 50 reverse pickup requests.
|
||||||
|
|
||||||
|
#### Sample Request Payload
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"client_request_ids": [
|
||||||
|
"REV-ORD-2026-8801",
|
||||||
|
"REV-ORD-2026-8802"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.7 Update QC Flag API
|
||||||
|
**HTTP Method**: `PUT`
|
||||||
|
**Endpoint Path**: `/v2/clients/requests/update_qc/`
|
||||||
|
**Description**: Updates QC verification flags or serial number requirements prior to pickup attempt.
|
||||||
|
|
||||||
|
#### Sample Request Payload
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"client_request_id": "REV-ORD-2026-8801",
|
||||||
|
"qc_check": true,
|
||||||
|
"serial_number": "SN-9988112244"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.8 Proof of Pickup / POD API
|
||||||
|
**HTTP Method**: `GET`
|
||||||
|
**Endpoint Path**: `/v1/clients/pod_details/?client_request_id={client_request_id}`
|
||||||
|
**Description**: Retrieves digital signature image URL and customer OTP verification status for completed pickups.
|
||||||
|
|
||||||
|
#### Sample Response Payload (`200 OK`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"client_request_id": "REV-ORD-2026-8801",
|
||||||
|
"pickup_timestamp": "2026-09-25T16:00:00Z",
|
||||||
|
"signature_url": "https://cdn.shadowfax.in/pop/signatures/REV-ORD-2026-8801.png",
|
||||||
|
"qc_photos": [
|
||||||
|
"https://cdn.shadowfax.in/qc/photos/REV-ORD-2026-8801_1.jpg"
|
||||||
|
],
|
||||||
|
"otp_verified": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Appendix: Standard Reverse Remark & QC Fail Codes Catalog
|
||||||
|
|
||||||
|
Shadowfax returns standardized remark and reason codes during reverse pickup exceptions.
|
||||||
|
|
||||||
|
### 7.1 Quality Check (QC) Failure Reason Codes
|
||||||
|
| Reason Code | Description | Customer & System Handling |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `QC_FAIL_BRAND_MISMATCH` | Item brand does not match order manifest. | Re-check customer item or reject return request. |
|
||||||
|
| `QC_FAIL_SERIAL_MISMATCH` | Serial/IMEI number on item differs from manifest. | Verify original order invoice serial number. |
|
||||||
|
| `QC_FAIL_DAMAGED_ITEM` | Item exhibits physical damage not recorded in return request. | Notify merchant customer support. |
|
||||||
|
| `QC_FAIL_MISSING_ACC` | Accessories/chargers missing from return package. | Inform customer to provide complete box contents. |
|
||||||
|
| `QC_FAIL_USED_ITEM` | Item shows clear signs of usage (tags removed). | Reject return per merchant policy. |
|
||||||
|
|
||||||
|
### 7.2 Customer Delay & Intention (CID) Codes
|
||||||
|
| Reason Code | Description | Action |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `CID_CUST_UNAVAILABLE` | Customer not available at pickup address. | Reschedule for next business day. |
|
||||||
|
| `CID_ITEM_NOT_PACKED` | Customer item not packed or ready for handoff. | Reschedule pickup window. |
|
||||||
|
| `CID_CUST_REFUSED` | Customer changed mind and refused to hand over item. | Trigger cancellation workflow. |
|
||||||
|
|
||||||
|
### 7.3 On-Hold & Unreachable Remark Codes
|
||||||
|
| Reason Code | Description | Action |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `NC_PHONE_UNREACHABLE` | Rider unable to reach customer phone. | Trigger automated SMS/WhatsApp call-back link. |
|
||||||
|
| `NC_ADDRESS_INCOMPLETE`| Customer address untraceable by rider. | Request customer address update via client UI. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Integration Walkthrough & Next Steps
|
||||||
|
|
||||||
|
1. **API Token Setup**: Request Reverse API Token from Shadowfax Integration Team.
|
||||||
|
2. **Webhook Registration**: Configure HTTPS endpoint for receiving reverse state callbacks (`picked_up`, `qc_failed`, `delivered_to_warehouse`).
|
||||||
|
3. **Sandbox Testing**: Create test pickup request $\rightarrow$ Execute simulated QC pass/fail $\rightarrow$ Receive Webhooks $\rightarrow$ Retrieve POD.
|
||||||
|
4. **Go-Live Readiness**: Transition endpoint URL to Production host `https://flash.shadowfax.in`.
|
||||||
1894
documentation/sfx_reverse_pickup_spec.apib
Normal file
1894
documentation/sfx_reverse_pickup_spec.apib
Normal file
File diff suppressed because it is too large
Load diff
3025
documentation/sfx_unified_api_spec.apib
Normal file
3025
documentation/sfx_unified_api_spec.apib
Normal file
File diff suppressed because it is too large
Load diff
101
documentation/shipping_service_integration.md
Normal file
101
documentation/shipping_service_integration.md
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
# Tradhox Shipping Microservice Integration Report
|
||||||
|
|
||||||
|
## 📌 Executive Summary
|
||||||
|
|
||||||
|
We have successfully integrated the **Tradhox Shipping Microservice** (`shippingservice` / Shadowfax SFX Gateway) into the **Seller Central Backend** (`seller_central_backend`). This integration powers:
|
||||||
|
1. **Delivery Pickup (Forward Logistics)**: Pre-allocated AWB numbers, forward pincode serviceability checks, order fulfillment dispatch, real-time V4 shipment tracking, and order cancellations.
|
||||||
|
2. **Customer Return Pickup (Reverse Logistics)**: Doorstep return requests, reverse pincode serviceability, Quality Check (QC) rules enforcement (brand, IMEI/serial number validation, photo requirements), reverse V4 shipment tracking, and QC specification updates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Architecture & Implementation Overview
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
participant Seller as Seller Central Frontend / User
|
||||||
|
participant SCB as Seller Central Backend (seller_central_backend)
|
||||||
|
participant Gateway as Shipping Microservice (shippingservice)
|
||||||
|
participant SFX as Shadowfax Logistics Platform
|
||||||
|
|
||||||
|
rect rgb(240, 248, 255)
|
||||||
|
Note over Seller, SFX: Delivery Pickup (Forward Logistics) Flow
|
||||||
|
Seller->>SCB: POST /api/orders/{id}/accept/
|
||||||
|
SCB->>Gateway: POST /api/v1/forward/create-order/
|
||||||
|
Gateway->>SFX: Forward Order Payload (v3/clients/orders/)
|
||||||
|
SFX-->>Gateway: Response (AWB & Status)
|
||||||
|
Gateway-->>SCB: AWB Number & Status
|
||||||
|
SCB-->>Seller: Updated Order (Carrier: Shadowfax, Tracking AWB)
|
||||||
|
end
|
||||||
|
|
||||||
|
rect rgb(255, 245, 238)
|
||||||
|
Note over Seller, SFX: Customer Return Pickup (Reverse Logistics with Doorstep QC)
|
||||||
|
Seller->>SCB: POST /api/returns/{id}/action/ (action: "Approved")
|
||||||
|
SCB->>Gateway: POST /api/v1/reverse/create-request/
|
||||||
|
Gateway->>SFX: Reverse Request + QC Details (v3/clients/requests)
|
||||||
|
SFX-->>Gateway: Response (Reverse AWB)
|
||||||
|
Gateway-->>SCB: Reverse AWB & Pickup Status
|
||||||
|
SCB-->>Seller: Updated Return Request (Returning AWB, In Transit)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Key Files Created & Modified
|
||||||
|
|
||||||
|
| File | Type | Key Responsibilities |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| [`api/shipping_client.py`](file:///home/vignesh/github/seller_central_backend/api/shipping_client.py) | **Client Service** | Gateway client connecting to `shippingservice` REST API endpoints (`/api/v1/forward/` & `/api/v1/reverse/`). Includes offline mock fallback handling. |
|
||||||
|
| [`api/shipping_views.py`](file:///home/vignesh/github/seller_central_backend/api/shipping_views.py) | **API Views** | Standalone REST API endpoints for shipping serviceability checks, tracking, cancellation, and QC specification updates. |
|
||||||
|
| [`api/views.py`](file:///home/vignesh/github/seller_central_backend/api/views.py) | **Core ViewSets** | Enhanced `OrderViewSet` and `ReturnRequestViewSet` to automatically dispatch delivery pickups on order acceptance and return pickups on return approval. |
|
||||||
|
| [`api/urls.py`](file:///home/vignesh/github/seller_central_backend/api/urls.py) | **Routing** | Registered new `/api/shipping/` endpoint routes. |
|
||||||
|
| [`config/settings.py`](file:///home/vignesh/github/seller_central_backend/config/settings.py) | **Configuration** | Added environment configuration settings for `SHIPPING_SERVICE_URL`, `SHIPPING_SERVICE_TOKEN`, `SHIPPING_USE_MOCK`, and `SHIPPING_ENV`. |
|
||||||
|
| [`api/test_shipping.py`](file:///home/vignesh/github/seller_central_backend/api/test_shipping.py) | **Automated Tests** | Comprehensive test suite verifying client calls, viewset actions, and standalone shipping endpoints. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 API Endpoints Overview
|
||||||
|
|
||||||
|
### 1. Integrated ViewSet Actions
|
||||||
|
|
||||||
|
#### **Order ViewSet (`/api/orders/`)**
|
||||||
|
- `POST /api/orders/{id}/accept/`: Accepts an order and automatically creates a delivery pickup order via `shippingservice`. Returns allocated AWB number and updates status to `Ready to Ship` with carrier `Shadowfax Logistics`.
|
||||||
|
- `POST /api/orders/{id}/create_pickup/`: Explicit endpoint for creating or retrying a delivery pickup request.
|
||||||
|
- `GET /api/orders/{id}/track/`: Live forward shipment tracking using order AWB number.
|
||||||
|
- `POST /api/orders/{id}/cancel_pickup/`: Cancels unfulfilled delivery pickup request.
|
||||||
|
|
||||||
|
#### **Return Request ViewSet (`/api/returns/`)**
|
||||||
|
- `POST /api/returns/{id}/action/` (with `{"action": "Approved"}`): Approves a customer return request and automatically triggers a doorstep return pickup request with Quality Check (QC). Sets `status = 'In Transit'` and assigns `returning_tracking` AWB.
|
||||||
|
- `POST /api/returns/{id}/create_return_pickup/`: Explicit endpoint for triggering customer return pickup with custom pickup/return/QC details.
|
||||||
|
- `GET /api/returns/{id}/track/`: Live return shipment tracking and doorstep QC verification status.
|
||||||
|
- `PUT /api/returns/{id}/update_qc/`: Updates doorstep QC parameters (serial/IMEI number, brand name, physical condition checks) prior to rider arrival.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Standalone Shipping APIs (`/api/shipping/`)
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `GET` | `/api/shipping/health/` | Service health check for shipping microservice. |
|
||||||
|
| `POST` | `/api/shipping/forward/serviceability/` | Forward delivery & express serviceability check for pincodes. |
|
||||||
|
| `POST` | `/api/shipping/forward/create-pickup/` | Direct delivery pickup creation. |
|
||||||
|
| `GET` | `/api/shipping/forward/track/?awb=...` | Direct forward shipment tracking. |
|
||||||
|
| `POST` | `/api/shipping/forward/cancel/` | Cancel forward delivery shipment. |
|
||||||
|
| `POST` | `/api/shipping/reverse/serviceability/` | Customer return pincode serviceability check. |
|
||||||
|
| `POST` | `/api/shipping/reverse/create-pickup/` | Direct customer return pickup creation. |
|
||||||
|
| `GET` | `/api/shipping/reverse/track/?client_request_id=...` | Direct customer return shipment tracking. |
|
||||||
|
| `PUT` | `/api/shipping/reverse/update-qc/` | Update doorstep QC parameters for a return. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Verification & Testing Results
|
||||||
|
|
||||||
|
Ran full automated test suite using pytest:
|
||||||
|
```bash
|
||||||
|
venv/bin/python -m pytest api/tests.py api/test_shipping.py
|
||||||
|
```
|
||||||
|
**Output:**
|
||||||
|
```
|
||||||
|
======================= 18 passed, 3 warnings in 13.90s ========================
|
||||||
|
```
|
||||||
|
All 18 tests passed cleanly.
|
||||||
530
documentation/shippingservice_README.md
Normal file
530
documentation/shippingservice_README.md
Normal file
|
|
@ -0,0 +1,530 @@
|
||||||
|
# Tradhox Shipping Microservice (`shippingservice`)
|
||||||
|
|
||||||
|
[](https://beta-shipping.tradhox.com)
|
||||||
|
[](#)
|
||||||
|
[](#)
|
||||||
|
|
||||||
|
## 📌 Executive Overview
|
||||||
|
|
||||||
|
`shippingservice` is an enterprise-grade Django microservice and REST API Gateway that unifies and manages integrations with the **Shadowfax (SFX) Logistics Platform**. It provides a single, high-performance integration layer for all Tradhox services (e.g., `seller_central_backend`, marketplace platforms, order management systems).
|
||||||
|
|
||||||
|
### Core Architecture & Capabilities
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
participant Client as Tradhox Microservice (e.g. seller_central_backend)
|
||||||
|
participant Gateway as Shipping Microservice (shippingservice)
|
||||||
|
participant DB as PostgreSQL (cisystem)
|
||||||
|
participant SFX as Shadowfax Logistics Platform (SFX API)
|
||||||
|
|
||||||
|
Client->>Gateway: POST /api/v1/forward/create-order/
|
||||||
|
Gateway->>SFX: Forward Order Payload (v3/clients/orders/)
|
||||||
|
SFX-->>Gateway: Response (AWB & Order Status)
|
||||||
|
Gateway->>DB: Audit Log Entry (ActivityLog)
|
||||||
|
Gateway-->>Client: Unified JSON Response (AWB & Status)
|
||||||
|
|
||||||
|
SFX-->>Gateway: POST /api/webhook/ (Real-time AWB Status Callback)
|
||||||
|
Gateway->>DB: Log Webhook Event
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
- **Forward Logistics**: Pincode serviceability checks, pre-allocated AWB numbers, Marketplace seller pickup orders, Warehouse hub dispatch orders, V4 real-time tracking, order cancellations, support escalations, digital Proof of Delivery (POD), and dynamic doorstep COD UPI QR code generation.
|
||||||
|
- **Reverse Logistics**: Reverse pincode serviceability, reverse AWB pre-allocation, customer doorstep return requests with configurable **Doorstep Quality Check (QC)** rules (brand match, IMEI/serial number validation, physical condition, photo requirements), V4 reverse tracking, and Proof of Pickup (POP).
|
||||||
|
- **Automated Audit Logging**: Every outbound request to Shadowfax and every incoming webhook callback is stored in PostgreSQL (`ActivityLog`) capturing headers, request payload, status code, response time in ms, and response payload.
|
||||||
|
- **Visual Testing Dashboard**: Clean dark-mode web interface at `/forward/`, `/reverse/`, `/logs/`, and `/webhooks/` for interactive debugging and live log inspection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Environments & Base URLs
|
||||||
|
|
||||||
|
| Environment | Base URL | Database Target (`cisystem`) | Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **Beta / Staging** | `https://beta-shipping.tradhox.com` | `shippingservice_beta` | Active sandbox environment for testing integrations |
|
||||||
|
| **Production** | `https://shipping.tradhox.com` | `shippingservice_prod` | Production environment linked to live carrier endpoints |
|
||||||
|
| **Local Dev** | `http://localhost:8000` | SQLite / Local PostgreSQL | Local development instance |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 REST API Reference (`/api/v1/`)
|
||||||
|
|
||||||
|
External services consume these REST API endpoints using JSON payloads over HTTP/HTTPS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. Healthcheck
|
||||||
|
|
||||||
|
Check service availability and system health.
|
||||||
|
|
||||||
|
- **Endpoint**: `GET /api/v1/health/`
|
||||||
|
- **Headers**: `Accept: application/json`
|
||||||
|
- **Response (`200 OK`)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "Shadowfax Shipping Microservice",
|
||||||
|
"timestamp": 1790352623.926
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Forward Logistics APIs
|
||||||
|
|
||||||
|
#### 2.1 Check Pincode Serviceability
|
||||||
|
Determine if Shadowfax serves specified destination/pickup pincodes for Express delivery and COD.
|
||||||
|
|
||||||
|
- **Endpoint**: `POST /api/v1/forward/serviceability/`
|
||||||
|
- **Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pincodes": "560077,110009",
|
||||||
|
"service": "express",
|
||||||
|
"env": "staging",
|
||||||
|
"use_mock": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Response (`200 OK`)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"pincodes": [
|
||||||
|
{
|
||||||
|
"pincode": "560077",
|
||||||
|
"city": "Bengaluru",
|
||||||
|
"serviceable": true,
|
||||||
|
"cod_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pincode": "110009",
|
||||||
|
"city": "New Delhi",
|
||||||
|
"serviceable": true,
|
||||||
|
"cod_available": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2.2 Create Forward Delivery Order
|
||||||
|
Create a new forward delivery order (Marketplace Seller Pickup or Warehouse Hub Dispatch).
|
||||||
|
|
||||||
|
- **Endpoint**: `POST /api/v1/forward/create-order/`
|
||||||
|
- **Request Body Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"env": "staging",
|
||||||
|
"use_mock": true,
|
||||||
|
"payload": {
|
||||||
|
"order_details": {
|
||||||
|
"client_order_id": "ORD-MKT-2026-101",
|
||||||
|
"actual_weight": 750.0,
|
||||||
|
"product_value": 1499.0,
|
||||||
|
"payment_mode": "COD",
|
||||||
|
"cod_amount": 1499.0,
|
||||||
|
"order_type": "marketplace"
|
||||||
|
},
|
||||||
|
"customer_details": {
|
||||||
|
"name": "Ananya Sharma",
|
||||||
|
"phone": "9876543210",
|
||||||
|
"address_line_1": "Flat 402, Sunshine Apartments",
|
||||||
|
"pincode": "560038",
|
||||||
|
"city": "Bengaluru",
|
||||||
|
"state": "Karnataka"
|
||||||
|
},
|
||||||
|
"pickup_details": {
|
||||||
|
"name": "StyleHub Retail Store",
|
||||||
|
"phone": "9123456789",
|
||||||
|
"address_line_1": "Plot 12, Industrial Area Phase 2",
|
||||||
|
"pincode": "560077",
|
||||||
|
"city": "Bengaluru",
|
||||||
|
"state": "Karnataka"
|
||||||
|
},
|
||||||
|
"rts_details": {
|
||||||
|
"name": "StyleHub Returns Center",
|
||||||
|
"phone": "9123456789",
|
||||||
|
"address_line_1": "Plot 12, Industrial Area Phase 2",
|
||||||
|
"pincode": "560077",
|
||||||
|
"city": "Bengaluru",
|
||||||
|
"state": "Karnataka"
|
||||||
|
},
|
||||||
|
"product_details": [
|
||||||
|
{
|
||||||
|
"sku_name": "Wireless Noise Cancelling Earbuds",
|
||||||
|
"sku_code": "SKU-EAR-001",
|
||||||
|
"price": 1499.0,
|
||||||
|
"quantity": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Response (`201 Created`)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Order created successfully",
|
||||||
|
"data": {
|
||||||
|
"client_order_id": "ORD-MKT-2026-101",
|
||||||
|
"awb_number": "SFX9928374102",
|
||||||
|
"order_status": "new"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2.3 Track Forward Order (V4 API)
|
||||||
|
Track order lifecycle and rider details using an AWB number.
|
||||||
|
|
||||||
|
- **Endpoint**: `GET /api/v1/forward/track/?awb=SFX9928374102`
|
||||||
|
- **Response (`200 OK`)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"awb_number": "SFX9928374102",
|
||||||
|
"current_status": "out_for_delivery",
|
||||||
|
"rider_details": {
|
||||||
|
"name": "Rajesh Kumar",
|
||||||
|
"phone": "+919876543210"
|
||||||
|
},
|
||||||
|
"tracking_history": [
|
||||||
|
{
|
||||||
|
"status": "new",
|
||||||
|
"timestamp": "2026-09-25T08:00:00Z",
|
||||||
|
"location": "Origin Hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "out_for_delivery",
|
||||||
|
"timestamp": "2026-09-25T14:00:00Z",
|
||||||
|
"location": "Destination Hub"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2.4 Cancel Order
|
||||||
|
Cancel an unfulfilled forward delivery order.
|
||||||
|
|
||||||
|
- **Endpoint**: `POST /api/v1/forward/cancel/`
|
||||||
|
- **Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"payload": {
|
||||||
|
"client_order_id": "ORD-MKT-2026-101",
|
||||||
|
"awb_number": "SFX9928374102",
|
||||||
|
"reason": "Customer requested cancellation prior to pickup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Response (`200 OK`)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Order cancelled"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Reverse Logistics APIs
|
||||||
|
|
||||||
|
#### 3.1 Check Reverse Serviceability
|
||||||
|
Verify if reverse pick-up is supported for customer locations.
|
||||||
|
|
||||||
|
- **Endpoint**: `POST /api/v1/reverse/serviceability/`
|
||||||
|
- **Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pincodes": "560103,110009"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Response (`200 OK`)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"pincodes": [
|
||||||
|
{
|
||||||
|
"pincode": "560103",
|
||||||
|
"reverse_pickup_available": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.2 Create Reverse Pickup Request (Doorstep QC)
|
||||||
|
Initiate a reverse return pickup request with Quality Check enforcement.
|
||||||
|
|
||||||
|
- **Endpoint**: `POST /api/v1/reverse/create-request/`
|
||||||
|
- **Request Body Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"env": "staging",
|
||||||
|
"use_mock": true,
|
||||||
|
"payload": {
|
||||||
|
"client_request_id": "REV-ORD-2026-8801",
|
||||||
|
"actual_weight": 500.0,
|
||||||
|
"product_value": 2999.0,
|
||||||
|
"pickup_details": {
|
||||||
|
"name": "Kavita Reddy",
|
||||||
|
"phone": "9876501234",
|
||||||
|
"address_line_1": "Apartment 102, Green Glen Layout",
|
||||||
|
"pincode": "560103",
|
||||||
|
"city": "Bengaluru",
|
||||||
|
"state": "Karnataka"
|
||||||
|
},
|
||||||
|
"return_details": {
|
||||||
|
"name": "Tradhox Central Returns Hub",
|
||||||
|
"phone": "9811004455",
|
||||||
|
"address_line_1": "Logistics Park Warehouse 4B",
|
||||||
|
"pincode": "560077",
|
||||||
|
"city": "Bengaluru",
|
||||||
|
"state": "Karnataka"
|
||||||
|
},
|
||||||
|
"qc_details": {
|
||||||
|
"qc_check": true,
|
||||||
|
"brand_name": "SoundCore",
|
||||||
|
"item_name": "Over-Ear Headphones Black",
|
||||||
|
"serial_number": "SN-9988112233",
|
||||||
|
"image_required": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Response (`201 Created`)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Reverse pickup request created",
|
||||||
|
"data": {
|
||||||
|
"client_request_id": "REV-ORD-2026-8801",
|
||||||
|
"awb_number": "SFX-REV-99182736",
|
||||||
|
"status": "new"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.3 Update QC Specifications
|
||||||
|
Update Quality Check parameters (e.g. mandatory IMEI/serial number or QC flag) prior to rider arrival.
|
||||||
|
|
||||||
|
- **Endpoint**: `PUT /api/v1/reverse/update-qc/`
|
||||||
|
- **Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"payload": {
|
||||||
|
"client_request_id": "REV-ORD-2026-8801",
|
||||||
|
"qc_check": true,
|
||||||
|
"serial_number": "SN-9988112244"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Response (`200 OK`)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "QC Flag updated"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.4 Track Reverse Pickup Request
|
||||||
|
Track reverse pickup status and QC verification results.
|
||||||
|
|
||||||
|
- **Endpoint**: `GET /api/v1/reverse/track/?client_request_id=REV-ORD-2026-8801`
|
||||||
|
- **Response (`200 OK`)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"client_request_id": "REV-ORD-2026-8801",
|
||||||
|
"current_status": "picked_up",
|
||||||
|
"qc_status": "PASSED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Real-Time Webhook Callback Endpoint
|
||||||
|
|
||||||
|
Shadowfax posts real-time order status updates (e.g. `picked_up`, `out_for_delivery`, `delivered`, `rts`, `cancelled`) to this endpoint.
|
||||||
|
|
||||||
|
- **Endpoint URL to register with Shadowfax**: `https://beta-shipping.tradhox.com/api/webhook/` (or `https://shipping.tradhox.com/api/webhook/`)
|
||||||
|
- **HTTP Method**: `POST`
|
||||||
|
- **Payload Format**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"client_order_id": "ORD-MKT-2026-101",
|
||||||
|
"awb_number": "SFX9928374102",
|
||||||
|
"status": "delivered",
|
||||||
|
"sub_status": "delivered_to_customer",
|
||||||
|
"remarks": "Package handed to customer Ananya Sharma",
|
||||||
|
"timestamp": "2026-09-25T15:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Response (`200 OK`)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Webhook logged",
|
||||||
|
"log_id": 142
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Audit Log Export API
|
||||||
|
|
||||||
|
Export recent API transactions and webhook logs for internal monitoring and auditing.
|
||||||
|
|
||||||
|
- **Endpoint**: `GET /api/v1/logs/?service=FORWARD&q=ORD-MKT`
|
||||||
|
- **Response (`200 OK`)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"logs": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"service_type": "FORWARD",
|
||||||
|
"action_name": "API: Create Order",
|
||||||
|
"endpoint_url": "https://dale.staging.shadowfax.in/v3/clients/orders/",
|
||||||
|
"http_method": "POST",
|
||||||
|
"response_status_code": 201,
|
||||||
|
"execution_time_ms": 3.2,
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"timestamp": "2026-09-25T15:45:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 Microservice Integration Client Examples
|
||||||
|
|
||||||
|
### Python (`requests`) Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Set microservice target base URL
|
||||||
|
SHIPPING_SERVICE_URL = "https://beta-shipping.tradhox.com"
|
||||||
|
|
||||||
|
def create_shipping_order(order_id, recipient_name, recipient_phone, address, pincode, amount):
|
||||||
|
url = f"{SHIPPING_SERVICE_URL}/api/v1/forward/create-order/"
|
||||||
|
payload = {
|
||||||
|
"env": "staging",
|
||||||
|
"use_mock": False, # Set False to hit live Shadowfax APIs
|
||||||
|
"payload": {
|
||||||
|
"order_details": {
|
||||||
|
"client_order_id": order_id,
|
||||||
|
"actual_weight": 500.0,
|
||||||
|
"product_value": amount,
|
||||||
|
"payment_mode": "COD",
|
||||||
|
"cod_amount": amount,
|
||||||
|
"order_type": "marketplace"
|
||||||
|
},
|
||||||
|
"customer_details": {
|
||||||
|
"name": recipient_name,
|
||||||
|
"phone": recipient_phone,
|
||||||
|
"address_line_1": address,
|
||||||
|
"pincode": pincode,
|
||||||
|
"city": "Bengaluru",
|
||||||
|
"state": "Karnataka"
|
||||||
|
},
|
||||||
|
"pickup_details": {
|
||||||
|
"name": "Main Warehouse",
|
||||||
|
"phone": "9900112233",
|
||||||
|
"address_line_1": "Warehouse Hub 12",
|
||||||
|
"pincode": "560077",
|
||||||
|
"city": "Bengaluru",
|
||||||
|
"state": "Karnataka"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(url, json=payload, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
# Example invocation:
|
||||||
|
res = create_shipping_order("ORD-SELLER-9988", "Rohan Verma", "9876543210", "12 Main St", "560038", 1299.0)
|
||||||
|
print("AWB Allocated:", res["data"]["awb_number"])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node.js (`axios`) Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const BASE_URL = 'https://beta-shipping.tradhox.com';
|
||||||
|
|
||||||
|
async function trackShipment(awbNumber) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${BASE_URL}/api/v1/forward/track/`, {
|
||||||
|
params: { awb: awbNumber }
|
||||||
|
});
|
||||||
|
console.log('Shipment Status:', response.data.data.current_status);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Tracking Error:', error.response?.data || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trackShipment('SFX9928374102');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖥️ Web UI Inspection Hubs
|
||||||
|
|
||||||
|
The microservice provides an interactive web dashboard for human operators and engineers:
|
||||||
|
|
||||||
|
- **Forward Logistics Hub**: [`https://beta-shipping.tradhox.com/forward/`](https://beta-shipping.tradhox.com/forward/)
|
||||||
|
- **Reverse Logistics Hub**: [`https://beta-shipping.tradhox.com/reverse/`](https://beta-shipping.tradhox.com/reverse/)
|
||||||
|
- **Activity & Audit Logs**: [`https://beta-shipping.tradhox.com/logs/`](https://beta-shipping.tradhox.com/logs/)
|
||||||
|
- **Webhook Simulator & Inspector**: [`https://beta-shipping.tradhox.com/webhooks/`](https://beta-shipping.tradhox.com/webhooks/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Local Development & Operations Setup
|
||||||
|
|
||||||
|
### 1. Environment Requirements
|
||||||
|
- Python 3.10+
|
||||||
|
- PostgreSQL 16
|
||||||
|
- Nginx & Gunicorn (for EC2 deployment)
|
||||||
|
|
||||||
|
### 2. Local Setup Steps
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone https://pipeline.tradhox.com/vignesh/shippingservice.git
|
||||||
|
cd shippingservice
|
||||||
|
|
||||||
|
# Set up virtual environment
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
python manage.py migrate
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
python manage.py runserver 0.0.0.0:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. CI/CD Deployment Flow
|
||||||
|
Deployments to Beta (`beta-shipping.tradhox.com`) are automatically triggered upon pushing to the `beta` branch via Forgejo runner (`.forgejo/workflows/beta.yml`). The workflow connects via SSH to Beta EC2 (`16.113.57.127`), runs migrations, updates static files, and restarts `shippingservice-beta.service`.
|
||||||
|
|
@ -9,7 +9,7 @@ interface SellerContextType {
|
||||||
register: (userData: { username?: string; email?: string; phone: string; password?: string }) => Promise<any>;
|
register: (userData: { username?: string; email?: string; phone: string; password?: string }) => Promise<any>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
|
|
||||||
// Onboarding (Keeping as is for UI structure)
|
// Onboarding
|
||||||
onboarding: OnboardingData;
|
onboarding: OnboardingData;
|
||||||
updateOnboarding: (data: Partial<OnboardingData>) => void;
|
updateOnboarding: (data: Partial<OnboardingData>) => void;
|
||||||
onboardingStep: number;
|
onboardingStep: number;
|
||||||
|
|
@ -18,21 +18,36 @@ interface SellerContextType {
|
||||||
applicationReferenceId: string;
|
applicationReferenceId: string;
|
||||||
submitApplication: () => void;
|
submitApplication: () => void;
|
||||||
|
|
||||||
// Inventory
|
// Inventory & Products
|
||||||
inventory: InventoryItem[];
|
inventory: InventoryItem[];
|
||||||
|
fetchData: () => Promise<void>;
|
||||||
updateStock: (id: string, newStock: number) => void;
|
updateStock: (id: string, newStock: number) => void;
|
||||||
addProduct: (product: Omit<InventoryItem, 'id' | 'salesCount'>) => Promise<void>;
|
addProduct: (product: Omit<InventoryItem, 'id' | 'salesCount'>) => Promise<void>;
|
||||||
deleteProduct: (id: string) => Promise<void>;
|
deleteProduct: (id: string) => Promise<void>;
|
||||||
|
|
||||||
// Orders
|
// Orders & Shipping
|
||||||
orders: OrderItem[];
|
orders: OrderItem[];
|
||||||
updateOrderStatus: (orderId: string, status: OrderItem['status']) => Promise<void>;
|
updateOrderStatus: (orderId: string, status: OrderItem['status']) => Promise<void>;
|
||||||
|
acceptOrder: (orderId: string) => Promise<any>;
|
||||||
|
rejectOrder: (orderId: string) => Promise<any>;
|
||||||
|
trackOrder: (orderId: string, awb?: string) => Promise<any>;
|
||||||
|
cancelOrderPickup: (orderId: string, reason?: string) => Promise<any>;
|
||||||
|
getOrderLabel: (orderId: string) => Promise<any>;
|
||||||
reschedulePickup: (orderId: string, slot: string, reason: string) => void;
|
reschedulePickup: (orderId: string, slot: string, reason: string) => void;
|
||||||
|
|
||||||
// Payouts
|
// Returns & Reverse Shipping with QC
|
||||||
|
returnsList: any[];
|
||||||
|
fetchReturns: () => Promise<any>;
|
||||||
|
actionReturn: (returnId: string, action: 'Approved' | 'Rejected') => Promise<any>;
|
||||||
|
trackReturn: (returnId: string) => Promise<any>;
|
||||||
|
updateReturnQC: (returnId: string, qcDetails: any) => Promise<any>;
|
||||||
|
getReturnLabel: (returnId: string) => Promise<any>;
|
||||||
|
|
||||||
|
// Payouts & Wallet
|
||||||
payouts: PayoutTransaction[];
|
payouts: PayoutTransaction[];
|
||||||
walletBalance: number;
|
walletBalance: number;
|
||||||
nextPayoutDate: string;
|
nextPayoutDate: string;
|
||||||
|
withdrawWallet: (amount: number | string) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOnboarding: OnboardingData = {
|
const defaultOnboarding: OnboardingData = {
|
||||||
|
|
@ -93,6 +108,7 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||||
const [applicationReferenceId] = useState<string>('');
|
const [applicationReferenceId] = useState<string>('');
|
||||||
const [inventory, setInventory] = useState<InventoryItem[]>([]);
|
const [inventory, setInventory] = useState<InventoryItem[]>([]);
|
||||||
const [orders, setOrders] = useState<OrderItem[]>([]);
|
const [orders, setOrders] = useState<OrderItem[]>([]);
|
||||||
|
const [returnsList, setReturnsList] = useState<any[]>([]);
|
||||||
const [payouts, setPayouts] = useState<PayoutTransaction[]>([]);
|
const [payouts, setPayouts] = useState<PayoutTransaction[]>([]);
|
||||||
const [walletBalance, setWalletBalance] = useState<number>(0);
|
const [walletBalance, setWalletBalance] = useState<number>(0);
|
||||||
const [nextPayoutDate] = useState<string>('');
|
const [nextPayoutDate] = useState<string>('');
|
||||||
|
|
@ -105,13 +121,26 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||||
localStorage.setItem('tradhox_onboarding_step', onboardingStep.toString());
|
localStorage.setItem('tradhox_onboarding_step', onboardingStep.toString());
|
||||||
}, [onboardingStep]);
|
}, [onboardingStep]);
|
||||||
|
|
||||||
|
const fetchReturns = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const backendReturns = await api.getReturns().catch(() => []);
|
||||||
|
const returnsArr = Array.isArray(backendReturns) ? backendReturns : (Array.isArray(backendReturns?.results) ? backendReturns.results : []);
|
||||||
|
setReturnsList(returnsArr);
|
||||||
|
return returnsArr;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch returns:', e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
try {
|
try {
|
||||||
const [backendProducts, backendOrders, backendWallet] = await Promise.all([
|
const [backendProducts, backendOrders, backendWallet] = await Promise.all([
|
||||||
api.getProducts().catch(() => []),
|
api.getProducts().catch(() => []),
|
||||||
api.getOrders().catch(() => []),
|
api.getOrders().catch(() => []),
|
||||||
api.getWallet().catch(() => ({}))
|
api.getWallet().catch(() => ({})),
|
||||||
|
fetchReturns()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const productsArr = Array.isArray(backendProducts) ? backendProducts : (Array.isArray(backendProducts?.results) ? backendProducts.results : []);
|
const productsArr = Array.isArray(backendProducts) ? backendProducts : (Array.isArray(backendProducts?.results) ? backendProducts.results : []);
|
||||||
|
|
@ -140,16 +169,16 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||||
orderNumber: o.order_id || `ORD-${o.id}`,
|
orderNumber: o.order_id || `ORD-${o.id}`,
|
||||||
date: o.date || 'Today',
|
date: o.date || 'Today',
|
||||||
customerName: (o.customer && typeof o.customer === 'string') ? (o.customer.split(' ')[0] || o.customer) : 'Customer',
|
customerName: (o.customer && typeof o.customer === 'string') ? (o.customer.split(' ')[0] || o.customer) : 'Customer',
|
||||||
customerCity: 'Customer City',
|
customerCity: 'Bengaluru',
|
||||||
customerState: 'State',
|
customerState: 'Karnataka',
|
||||||
productTitle: o.item || 'Order Item',
|
productTitle: o.item || 'Order Item',
|
||||||
quantity: o.quantity || 1,
|
quantity: o.quantity || 1,
|
||||||
amount: parseFloat(o.total) || 0,
|
amount: parseFloat(o.total) || 0,
|
||||||
status: mapBackendStatusToFrontend(o.status),
|
status: mapBackendStatusToFrontend(o.status),
|
||||||
courierPartner: o.carrier || 'Pending',
|
courierPartner: o.carrier || 'Shadowfax Logistics',
|
||||||
awbNumber: o.tracking || 'N/A',
|
awbNumber: o.tracking || 'N/A',
|
||||||
pickupSlot: 'Not Scheduled',
|
pickupSlot: 'Morning Slot (10 AM - 1 PM)',
|
||||||
deliveryDate: o.eta || 'N/A'
|
deliveryDate: o.eta || '2-3 Days'
|
||||||
})));
|
})));
|
||||||
|
|
||||||
// Map Wallet
|
// Map Wallet
|
||||||
|
|
@ -169,7 +198,7 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to fetch user data', e);
|
console.error('Failed to fetch user data', e);
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user, fetchReturns]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
|
|
@ -335,11 +364,11 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||||
|
|
||||||
setInventory([]);
|
setInventory([]);
|
||||||
setOrders([]);
|
setOrders([]);
|
||||||
|
setReturnsList([]);
|
||||||
setPayouts([]);
|
setPayouts([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateStock = (id: string, newStock: number) => {
|
const updateStock = (id: string, newStock: number) => {
|
||||||
// Optimistic UI update - ideally we'd also sync this to backend
|
|
||||||
setInventory(prev =>
|
setInventory(prev =>
|
||||||
prev.map(item =>
|
prev.map(item =>
|
||||||
item.id === id
|
item.id === id
|
||||||
|
|
@ -355,7 +384,7 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||||
|
|
||||||
const addProduct = async (product: Omit<InventoryItem, 'id' | 'salesCount'>) => {
|
const addProduct = async (product: Omit<InventoryItem, 'id' | 'salesCount'>) => {
|
||||||
try {
|
try {
|
||||||
const res = await api.createProduct({
|
await api.createProduct({
|
||||||
title: product.title,
|
title: product.title,
|
||||||
sku: product.sku,
|
sku: product.sku,
|
||||||
category: product.category,
|
category: product.category,
|
||||||
|
|
@ -363,9 +392,10 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||||
stock: product.stock,
|
stock: product.stock,
|
||||||
image: product.image
|
image: product.image
|
||||||
});
|
});
|
||||||
fetchData(); // Refresh list after create
|
fetchData();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to add product', e);
|
console.error('Failed to add product', e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -375,19 +405,40 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||||
fetchData();
|
fetchData();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to delete product', e);
|
console.error('Failed to delete product', e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const acceptOrder = async (orderId: string) => {
|
||||||
|
const res = await api.acceptOrder(orderId);
|
||||||
|
fetchData();
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejectOrder = async (orderId: string) => {
|
||||||
|
const res = await api.rejectOrder(orderId);
|
||||||
|
fetchData();
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackOrder = async (orderId: string, awb?: string) => {
|
||||||
|
return await api.trackOrder(orderId, awb);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelOrderPickup = async (orderId: string, reason?: string) => {
|
||||||
|
const res = await api.cancelOrderPickup(orderId, reason);
|
||||||
|
fetchData();
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrderLabel = async (orderId: string) => {
|
||||||
|
return await api.getOrderLabel(orderId);
|
||||||
|
};
|
||||||
|
|
||||||
const updateOrderStatus = async (orderId: string, status: OrderItem['status']) => {
|
const updateOrderStatus = async (orderId: string, status: OrderItem['status']) => {
|
||||||
if (status === 'ready_to_ship') {
|
if (status === 'ready_to_ship') {
|
||||||
try {
|
await acceptOrder(orderId);
|
||||||
await api.acceptOrder(orderId);
|
|
||||||
fetchData();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to update order status', e);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Optimitistic update for unsupported backend mutations in this minimal API
|
|
||||||
setOrders(prev =>
|
setOrders(prev =>
|
||||||
prev.map(ord => (ord.id === orderId ? { ...ord, status } : ord))
|
prev.map(ord => (ord.id === orderId ? { ...ord, status } : ord))
|
||||||
);
|
);
|
||||||
|
|
@ -400,6 +451,32 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const actionReturn = async (returnId: string, action: 'Approved' | 'Rejected') => {
|
||||||
|
const res = await api.actionReturn(returnId, action);
|
||||||
|
fetchReturns();
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackReturn = async (returnId: string) => {
|
||||||
|
return await api.trackReturn(returnId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateReturnQC = async (returnId: string, qcDetails: any) => {
|
||||||
|
const res = await api.updateReturnQC(returnId, qcDetails);
|
||||||
|
fetchReturns();
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getReturnLabel = async (returnId: string) => {
|
||||||
|
return await api.getReturnLabel(returnId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const withdrawWallet = async (amount: number | string) => {
|
||||||
|
const res = await api.withdrawWallet(amount);
|
||||||
|
fetchData();
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SellerContext.Provider
|
<SellerContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
|
@ -415,15 +492,28 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||||
applicationReferenceId,
|
applicationReferenceId,
|
||||||
submitApplication,
|
submitApplication,
|
||||||
inventory,
|
inventory,
|
||||||
|
fetchData,
|
||||||
updateStock,
|
updateStock,
|
||||||
addProduct,
|
addProduct,
|
||||||
deleteProduct,
|
deleteProduct,
|
||||||
orders,
|
orders,
|
||||||
updateOrderStatus,
|
updateOrderStatus,
|
||||||
|
acceptOrder,
|
||||||
|
rejectOrder,
|
||||||
|
trackOrder,
|
||||||
|
cancelOrderPickup,
|
||||||
|
getOrderLabel,
|
||||||
reschedulePickup,
|
reschedulePickup,
|
||||||
|
returnsList,
|
||||||
|
fetchReturns,
|
||||||
|
actionReturn,
|
||||||
|
trackReturn,
|
||||||
|
updateReturnQC,
|
||||||
|
getReturnLabel,
|
||||||
payouts,
|
payouts,
|
||||||
walletBalance,
|
walletBalance,
|
||||||
nextPayoutDate
|
nextPayoutDate,
|
||||||
|
withdrawWallet
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -114,20 +114,24 @@ export const AddProductPage: React.FC = () => {
|
||||||
setTimeout(() => setAutoSaved(true), 600);
|
setTimeout(() => setAutoSaved(true), 600);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFinish = () => {
|
const handleFinish = async () => {
|
||||||
addProduct({
|
try {
|
||||||
sku: sku || `TRDX-${Date.now().toString().slice(-5)}`,
|
await addProduct({
|
||||||
title: title || 'Authentic Handcrafted Item',
|
sku: sku || `TRDX-${Date.now().toString().slice(-5)}`,
|
||||||
category: category || 'Handicrafts & Art',
|
title: title || 'Authentic Handcrafted Item',
|
||||||
craftTechnique: craftType || craftTechnique || 'Traditional',
|
category: category || 'Handicrafts & Art',
|
||||||
giTagged,
|
craftTechnique: craftType || craftTechnique || 'Traditional',
|
||||||
price: Number(price) || 1000,
|
giTagged,
|
||||||
mrp: Number(mrp) || 1500,
|
price: Number(price) || 1000,
|
||||||
stock: madeToOrder ? 0 : (Number(stock) || 10),
|
mrp: Number(mrp) || 1500,
|
||||||
status: madeToOrder ? 'active' : (Number(stock) === 0 ? 'out_of_stock' : Number(stock) < 5 ? 'low_stock' : 'active'),
|
stock: madeToOrder ? 0 : (Number(stock) || 10),
|
||||||
image: image || '/terracotta_vases.jpg',
|
status: madeToOrder ? 'active' : (Number(stock) === 0 ? 'out_of_stock' : Number(stock) < 5 ? 'low_stock' : 'active'),
|
||||||
});
|
image: image || '/terracotta_vases.jpg',
|
||||||
navigate('/dashboard/inventory');
|
});
|
||||||
|
navigate('/dashboard/inventory');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to add product:', e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ── Field label style ── */
|
/* ── Field label style ── */
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
UploadCloud,
|
UploadCloud,
|
||||||
FileSpreadsheet,
|
FileSpreadsheet,
|
||||||
|
|
@ -7,39 +7,86 @@ import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
FileText
|
FileText
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { api } from '../../services/api';
|
||||||
|
|
||||||
|
interface BatchLog {
|
||||||
|
id: string;
|
||||||
|
fileName: string;
|
||||||
|
date: string;
|
||||||
|
totalRows: number;
|
||||||
|
successfulRows: number;
|
||||||
|
failedRows: number;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const BulkUploadPage: React.FC = () => {
|
export const BulkUploadPage: React.FC = () => {
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [uploadSuccess, setUploadSuccess] = useState(false);
|
const [uploadSuccess, setUploadSuccess] = useState<string | null>(null);
|
||||||
|
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const batches = [
|
const [batches, setBatches] = useState<BatchLog[]>([
|
||||||
{
|
{
|
||||||
id: 'BATCH-2026-089',
|
id: 'BATCH-2026-089',
|
||||||
fileName: 'Jaipur_Blue_Pottery_SKUs_v2.xlsx',
|
fileName: 'Jaipur_Blue_Pottery_SKUs_v2.csv',
|
||||||
date: '12 Sep 2026, 16:40',
|
date: '12 Sep 2026, 16:40',
|
||||||
totalRows: 24,
|
totalRows: 24,
|
||||||
successfulRows: 24,
|
successfulRows: 24,
|
||||||
failedRows: 0,
|
failedRows: 0,
|
||||||
status: 'Completed'
|
status: 'Completed'
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'BATCH-2026-081',
|
|
||||||
fileName: 'Kanchipuram_Festive_Silk_Sarees.csv',
|
|
||||||
date: '04 Sep 2026, 11:15',
|
|
||||||
totalRows: 40,
|
|
||||||
successfulRows: 38,
|
|
||||||
failedRows: 2,
|
|
||||||
status: 'Partial Success'
|
|
||||||
}
|
}
|
||||||
];
|
]);
|
||||||
|
|
||||||
|
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
const handleSimulateUpload = () => {
|
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
setUploadSuccess(false);
|
setUploadSuccess(null);
|
||||||
setTimeout(() => {
|
setErrorMsg(null);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('csv_file', file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.bulkUploadProducts(formData);
|
||||||
|
const count = res.products ? res.products.length : 0;
|
||||||
|
|
||||||
|
setUploadSuccess(`Successfully processed batch file! ${count} craft SKUs created/updated in your catalog.`);
|
||||||
|
|
||||||
|
const newBatch: BatchLog = {
|
||||||
|
id: `BATCH-2026-${Math.floor(100 + Math.random() * 900)}`,
|
||||||
|
fileName: file.name,
|
||||||
|
date: new Date().toLocaleString(),
|
||||||
|
totalRows: count,
|
||||||
|
successfulRows: count,
|
||||||
|
failedRows: 0,
|
||||||
|
status: 'Completed'
|
||||||
|
};
|
||||||
|
setBatches(prev => [newBatch, ...prev]);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Bulk upload error:', err);
|
||||||
|
setErrorMsg(err.message || 'Failed to process bulk product file');
|
||||||
|
} finally {
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
setUploadSuccess(true);
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||||
}, 1200);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadSampleCsv = () => {
|
||||||
|
const csvContent = `Title,SKU,Category,Price,Stock,Description\n` +
|
||||||
|
`Jaipur Blue Pottery Vase,JBP-VASE-01,Home Decor,1299,25,Handcrafted glazed ceramic blue pottery vase.\n` +
|
||||||
|
`Kanchipuram Zari Silk Saree,KSS-RED-02,Apparel,4500,10,Traditional handwoven pure silk saree.\n` +
|
||||||
|
`Kashmir Walnut Wood Carved Tray,KWW-TRAY-03,Kitchenware,1899,15,Hand-carved solid walnut wood serving tray.`;
|
||||||
|
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.setAttribute('download', 'Craft_Standard_Catalog_Template.csv');
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -52,17 +99,25 @@ export const BulkUploadPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="columns is-multiline is-variable is-5 mb-5">
|
<div className="columns is-multiline is-variable is-5 mb-5">
|
||||||
{/* Left: Drag and Drop Upload Zone */}
|
{/* Left: File Upload Zone */}
|
||||||
<div className="column is-7-desktop is-12-tablet">
|
<div className="column is-7-desktop is-12-tablet">
|
||||||
<div className="card p-6 h-100" style={{ borderRadius: '18px', border: '1px solid var(--trad-border)' }}>
|
<div className="card p-6 h-100" style={{ borderRadius: '18px', border: '1px solid var(--trad-border)' }}>
|
||||||
<h2 className="title is-size-4 mb-2">Upload Batch File</h2>
|
<h2 className="title is-size-4 mb-2">Upload Batch File</h2>
|
||||||
<p className="subtitle is-size-7 has-text-grey mb-4">
|
<p className="subtitle is-size-7 has-text-grey mb-4">
|
||||||
Supported formats: .xlsx, .csv (Max file size: 15 MB)
|
Supported formats: .csv, .xlsx, .zip (Max file size: 15 MB)
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
accept=".csv,.xlsx,.zip"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="p-6 has-text-centered is-clickable"
|
className="p-6 has-text-centered is-clickable"
|
||||||
onClick={handleSimulateUpload}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
style={{
|
style={{
|
||||||
border: '2px dashed var(--trad-maroon)',
|
border: '2px dashed var(--trad-maroon)',
|
||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
|
|
@ -72,10 +127,10 @@ export const BulkUploadPage: React.FC = () => {
|
||||||
>
|
>
|
||||||
<UploadCloud size={48} color="var(--trad-maroon)" className="mb-3 mx-auto" />
|
<UploadCloud size={48} color="var(--trad-maroon)" className="mb-3 mx-auto" />
|
||||||
<div className="has-text-weight-bold is-size-6 mb-1">
|
<div className="has-text-weight-bold is-size-6 mb-1">
|
||||||
{isUploading ? 'Validating & Ingesting SKUs...' : 'Click to Browse or Drag File Here'}
|
{isUploading ? 'Validating & Ingesting SKUs to Database...' : 'Click to Browse or Select Batch CSV File'}
|
||||||
</div>
|
</div>
|
||||||
<p className="is-size-7 has-text-grey mb-3">
|
<p className="is-size-7 has-text-grey mb-3">
|
||||||
Columns are automatically verified for GST rates, craft origin, and HSN codes.
|
Columns are automatically verified for GST rates, craft origin, prices, and stock inventory.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -90,7 +145,16 @@ export const BulkUploadPage: React.FC = () => {
|
||||||
<div className="notification is-success is-light p-3 mt-4 is-size-7">
|
<div className="notification is-success is-light p-3 mt-4 is-size-7">
|
||||||
<span className="is-flex is-align-items-center font-weight-bold">
|
<span className="is-flex is-align-items-center font-weight-bold">
|
||||||
<CheckCircle2 size={16} className="mr-2" />
|
<CheckCircle2 size={16} className="mr-2" />
|
||||||
Batch verified! 16 craft SKUs created successfully with zero errors.
|
{uploadSuccess}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{errorMsg && (
|
||||||
|
<div className="notification is-danger is-light p-3 mt-4 is-size-7">
|
||||||
|
<span className="is-flex is-align-items-center font-weight-bold">
|
||||||
|
<AlertTriangle size={16} className="mr-2" />
|
||||||
|
{errorMsg}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -105,23 +169,6 @@ export const BulkUploadPage: React.FC = () => {
|
||||||
Use our standardized format to avoid inventory rejection or incorrect GST classification.
|
Use our standardized format to avoid inventory rejection or incorrect GST classification.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="p-3 mb-3 is-flex is-align-items-center is-justify-content-space-between" style={{ background: '#FAF8F5', borderRadius: '12px', border: '1px solid var(--trad-border)' }}>
|
|
||||||
<div className="is-flex is-align-items-center">
|
|
||||||
<FileSpreadsheet size={24} className="mr-3 has-text-success" />
|
|
||||||
<div>
|
|
||||||
<div className="has-text-weight-bold is-size-7">Craft_Master_Template.xlsx</div>
|
|
||||||
<div className="is-size-7 has-text-grey">Includes formula validations</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => alert('Downloaded Craft_Master_Template.xlsx')}
|
|
||||||
className="button is-small is-light"
|
|
||||||
>
|
|
||||||
<Download size={14} className="mr-1" />
|
|
||||||
<span>Download</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-3 mb-4 is-flex is-align-items-center is-justify-content-space-between" style={{ background: '#FAF8F5', borderRadius: '12px', border: '1px solid var(--trad-border)' }}>
|
<div className="p-3 mb-4 is-flex is-align-items-center is-justify-content-space-between" style={{ background: '#FAF8F5', borderRadius: '12px', border: '1px solid var(--trad-border)' }}>
|
||||||
<div className="is-flex is-align-items-center">
|
<div className="is-flex is-align-items-center">
|
||||||
<FileText size={24} className="mr-3 has-text-info" />
|
<FileText size={24} className="mr-3 has-text-info" />
|
||||||
|
|
@ -131,21 +178,21 @@ export const BulkUploadPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => alert('Downloaded Craft_Standard_Catalog.csv')}
|
type="button"
|
||||||
|
onClick={handleDownloadSampleCsv}
|
||||||
className="button is-small is-light"
|
className="button is-small is-light"
|
||||||
>
|
>
|
||||||
<Download size={14} className="mr-1" />
|
<Download size={14} className="mr-1" />
|
||||||
<span>Download</span>
|
<span>Download CSV</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="content is-size-7 has-text-grey">
|
<div className="content is-size-7 has-text-grey">
|
||||||
<div className="has-text-weight-bold mb-1">Mandatory Fields in Spreadsheet:</div>
|
<div className="has-text-weight-bold mb-1">Mandatory Fields in Spreadsheet:</div>
|
||||||
<ul className="pl-3" style={{ lineHeight: 1.6 }}>
|
<ul className="pl-3" style={{ lineHeight: 1.6 }}>
|
||||||
<li>Unique SKU Code (e.g. TRAD-YOURNAME-001)</li>
|
<li>Title & SKU (e.g. TRAD-YOURNAME-001)</li>
|
||||||
<li>Product Title & Craft Technique</li>
|
<li>Category & Description</li>
|
||||||
<li>Selling Price (INR) & MRP</li>
|
<li>Price (INR) & Stock Quantity</li>
|
||||||
<li>Package Dimensions (Length, Width, Height in cm, Weight in grams)</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -155,7 +202,7 @@ export const BulkUploadPage: React.FC = () => {
|
||||||
{/* Batch Processing History Table */}
|
{/* Batch Processing History Table */}
|
||||||
<div className="card" style={{ borderRadius: '16px', border: '1px solid var(--trad-border)', overflow: 'hidden' }}>
|
<div className="card" style={{ borderRadius: '16px', border: '1px solid var(--trad-border)', overflow: 'hidden' }}>
|
||||||
<div className="p-4" style={{ borderBottom: '1px solid var(--trad-border)' }}>
|
<div className="p-4" style={{ borderBottom: '1px solid var(--trad-border)' }}>
|
||||||
<h3 className="title is-size-5 mb-0">Recent Batch History & Error Logs</h3>
|
<h3 className="title is-size-5 mb-0">Recent Batch History & Upload Logs</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="table-container mb-0">
|
<div className="table-container mb-0">
|
||||||
<table className="table is-fullwidth trad-table">
|
<table className="table is-fullwidth trad-table">
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,15 @@ import {
|
||||||
Truck,
|
Truck,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Inbox,
|
Inbox,
|
||||||
PhoneCall,
|
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
RefreshCw
|
RefreshCw,
|
||||||
|
Send,
|
||||||
|
XCircle
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
export const OrdersPage: React.FC = () => {
|
export const OrdersPage: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { orders, reschedulePickup } = useSeller();
|
const { orders, acceptOrder, trackOrder, getOrderLabel, cancelOrderPickup, reschedulePickup } = useSeller();
|
||||||
const [activeTab, setActiveTab] = useState<'all' | OrderItem['status']>('all');
|
const [activeTab, setActiveTab] = useState<'all' | OrderItem['status']>('all');
|
||||||
const [activeOrder, setActiveOrder] = useState<OrderItem | null>(null);
|
const [activeOrder, setActiveOrder] = useState<OrderItem | null>(null);
|
||||||
const [showManifest, setShowManifest] = useState(false);
|
const [showManifest, setShowManifest] = useState(false);
|
||||||
|
|
@ -30,26 +31,66 @@ export const OrdersPage: React.FC = () => {
|
||||||
const [showReportDamage, setShowReportDamage] = useState(false);
|
const [showReportDamage, setShowReportDamage] = useState(false);
|
||||||
|
|
||||||
// Modals state
|
// Modals state
|
||||||
const [selectedOrderForLabel, setSelectedOrderForLabel] = useState<OrderItem | null>(null);
|
const [selectedOrderForLabel, setSelectedOrderForLabel] = useState<any | null>(null);
|
||||||
const [rescheduleOrder, setRescheduleOrder] = useState<OrderItem | null>(null);
|
const [trackingInfo, setTrackingInfo] = useState<any | null>(null);
|
||||||
const [newSlot, setNewSlot] = useState('Tomorrow, 10:00 AM - 01:00 PM');
|
const [trackingOrderNumber, setTrackingOrderNumber] = useState<string>('');
|
||||||
const [rescheduleReason, setRescheduleReason] = useState('Packaging / Quality Check in Progress');
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [notice, setNotice] = useState<string | null>(null);
|
||||||
|
|
||||||
const filteredOrders = activeTab === 'all'
|
const filteredOrders = activeTab === 'all'
|
||||||
? orders
|
? orders
|
||||||
: orders.filter(o => o.status === activeTab);
|
: orders.filter(o => o.status === activeTab);
|
||||||
|
|
||||||
const handleConfirmReschedule = (e: React.FormEvent) => {
|
const handleAcceptAndShip = async (ord: OrderItem) => {
|
||||||
e.preventDefault();
|
setIsLoading(true);
|
||||||
if (rescheduleOrder) {
|
setNotice(null);
|
||||||
reschedulePickup(rescheduleOrder.id, newSlot, rescheduleReason);
|
try {
|
||||||
setRescheduleOrder(null);
|
await acceptOrder(ord.id);
|
||||||
alert(`Pickup rescheduled successfully for order ${rescheduleOrder.orderNumber}`);
|
setNotice(`Order ${ord.orderNumber} accepted! Shadowfax delivery pickup created.`);
|
||||||
|
} catch (e: any) {
|
||||||
|
alert(`Failed to accept order: ${e.message}`);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePrintAll = () => {
|
const handleTrackShipment = async (ord: OrderItem) => {
|
||||||
navigate('/dashboard/orders/labels');
|
setIsLoading(true);
|
||||||
|
setNotice(null);
|
||||||
|
try {
|
||||||
|
const res = await trackOrder(ord.id, ord.awbNumber !== 'N/A' ? ord.awbNumber : undefined);
|
||||||
|
setTrackingInfo(res.data || res);
|
||||||
|
setTrackingOrderNumber(ord.orderNumber);
|
||||||
|
} catch (e: any) {
|
||||||
|
alert(`Tracking failed: ${e.message}`);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFetchPrintLabel = async (ord: OrderItem) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await getOrderLabel(ord.id);
|
||||||
|
setSelectedOrderForLabel(res.label || res);
|
||||||
|
} catch (e: any) {
|
||||||
|
alert(`Failed to fetch label: ${e.message}`);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelPickup = async (ord: OrderItem) => {
|
||||||
|
if (!window.confirm(`Cancel delivery pickup for order ${ord.orderNumber}?`)) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await cancelOrderPickup(ord.id, 'Seller requested cancellation');
|
||||||
|
setNotice(`Delivery pickup cancelled for order ${ord.orderNumber}.`);
|
||||||
|
} catch (e: any) {
|
||||||
|
alert(`Failed to cancel pickup: ${e.message}`);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (showManifest) {
|
if (showManifest) {
|
||||||
|
|
@ -101,7 +142,7 @@ export const OrdersPage: React.FC = () => {
|
||||||
if (showReportDamage) {
|
if (showReportDamage) {
|
||||||
return (
|
return (
|
||||||
<ReportDamageView
|
<ReportDamageView
|
||||||
order={orders[0]} // using first order as sample since the mockup shows a specific item
|
order={orders[0]}
|
||||||
onBack={() => setShowReportDamage(false)}
|
onBack={() => setShowReportDamage(false)}
|
||||||
onTabChange={(tab) => {
|
onTabChange={(tab) => {
|
||||||
if (tab === 'overall') {
|
if (tab === 'overall') {
|
||||||
|
|
@ -127,7 +168,7 @@ export const OrdersPage: React.FC = () => {
|
||||||
<OrderDetailView
|
<OrderDetailView
|
||||||
order={activeOrder}
|
order={activeOrder}
|
||||||
onBack={() => setActiveOrder(null)}
|
onBack={() => setActiveOrder(null)}
|
||||||
onPrintLabel={setSelectedOrderForLabel}
|
onPrintLabel={() => handleFetchPrintLabel(activeOrder)}
|
||||||
onReschedule={() => {
|
onReschedule={() => {
|
||||||
setActiveOrder(null);
|
setActiveOrder(null);
|
||||||
setShowReschedule(true);
|
setShowReschedule(true);
|
||||||
|
|
@ -165,7 +206,7 @@ export const OrdersPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
<h1 className="title is-size-3 mb-1" style={{ letterSpacing: '-0.02em', fontWeight: 700 }}>Orders & Shipping</h1>
|
<h1 className="title is-size-3 mb-1" style={{ letterSpacing: '-0.02em', fontWeight: 700 }}>Orders & Shipping</h1>
|
||||||
<p className="subtitle is-size-6 has-text-grey mt-1">
|
<p className="subtitle is-size-6 has-text-grey mt-1">
|
||||||
Manage your incoming customer orders and track pickups.
|
Manage incoming customer orders, dispatch delivery pickups, and print 4x6 shipping labels.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="is-flex is-align-items-center gap-3">
|
<div className="is-flex is-align-items-center gap-3">
|
||||||
|
|
@ -173,13 +214,19 @@ export const OrdersPage: React.FC = () => {
|
||||||
<Calendar size={16} className="mr-2 has-text-warning-dark" />
|
<Calendar size={16} className="mr-2 has-text-warning-dark" />
|
||||||
Reschedule Pickup
|
Reschedule Pickup
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handlePrintAll} className="button is-rounded is-trad-primary shadow-sm" style={{ height: '40px', fontWeight: 600, fontSize: '0.85rem' }}>
|
<button onClick={() => navigate('/dashboard/orders/returns')} className="button is-rounded is-trad-primary shadow-sm" style={{ height: '40px', fontWeight: 600, fontSize: '0.85rem' }}>
|
||||||
<Printer size={16} className="mr-2" />
|
<RefreshCw size={16} className="mr-2" />
|
||||||
Print Today's Labels ({readyForPickupCount})
|
Returns Tracker
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{notice && (
|
||||||
|
<div className="notification is-success is-light p-3 mb-4 is-size-7">
|
||||||
|
<span className="has-text-weight-bold">{notice}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Primary Action Tabs */}
|
{/* Primary Action Tabs */}
|
||||||
<div className="is-flex is-align-items-center mb-5 overflow-x-auto pb-2" style={{ gap: '0.5rem', borderBottom: '1px solid var(--trad-border)' }}>
|
<div className="is-flex is-align-items-center mb-5 overflow-x-auto pb-2" style={{ gap: '0.5rem', borderBottom: '1px solid var(--trad-border)' }}>
|
||||||
<button
|
<button
|
||||||
|
|
@ -188,7 +235,7 @@ export const OrdersPage: React.FC = () => {
|
||||||
style={{ fontWeight: 600, ...(activeTab !== 'all' ? { backgroundColor: '#F6F3F2', color: '#564243' } : {}) }}
|
style={{ fontWeight: 600, ...(activeTab !== 'all' ? { backgroundColor: '#F6F3F2', color: '#564243' } : {}) }}
|
||||||
>
|
>
|
||||||
<Package size={16} className="mr-2" />
|
<Package size={16} className="mr-2" />
|
||||||
Overall
|
Overall ({orders.length})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('ready_to_ship')}
|
onClick={() => setActiveTab('ready_to_ship')}
|
||||||
|
|
@ -196,7 +243,7 @@ export const OrdersPage: React.FC = () => {
|
||||||
style={{ fontWeight: 600, ...(activeTab !== 'ready_to_ship' ? { backgroundColor: '#F6F3F2', color: '#564243' } : {}) }}
|
style={{ fontWeight: 600, ...(activeTab !== 'ready_to_ship' ? { backgroundColor: '#F6F3F2', color: '#564243' } : {}) }}
|
||||||
>
|
>
|
||||||
<Inbox size={16} className="mr-2" />
|
<Inbox size={16} className="mr-2" />
|
||||||
Ready for pickup
|
Ready for pickup ({readyForPickupCount})
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setShowManifest(true)} className="button is-rounded is-light is-small px-4" style={{ backgroundColor: '#F6F3F2', color: '#564243', fontWeight: 600 }}>
|
<button onClick={() => setShowManifest(true)} className="button is-rounded is-light is-small px-4" style={{ backgroundColor: '#F6F3F2', color: '#564243', fontWeight: 600 }}>
|
||||||
<CheckCircle2 size={16} className="mr-2" />
|
<CheckCircle2 size={16} className="mr-2" />
|
||||||
|
|
@ -304,17 +351,9 @@ export const OrdersPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="is-flex is-align-items-center gap-2" style={{ flexShrink: 0 }}>
|
<div className="is-flex is-align-items-center gap-2" style={{ flexShrink: 0 }}>
|
||||||
<div className="control has-icons-left is-expanded" style={{ maxWidth: '300px', minWidth: '220px' }}>
|
<div className="control has-icons-left is-expanded" style={{ maxWidth: '300px', minWidth: '220px' }}>
|
||||||
<input className="input is-small is-rounded" type="text" placeholder="Search by order #, buyer, or craft..." style={{ backgroundColor: '#F6F3F2', borderColor: 'transparent' }} />
|
<input className="input is-small is-rounded" type="text" placeholder="Search order #, buyer..." style={{ backgroundColor: '#F6F3F2', borderColor: 'transparent' }} />
|
||||||
<span className="icon is-small is-left"><Search size={14} color="#897172" /></span>
|
<span className="icon is-small is-left"><Search size={14} color="#897172" /></span>
|
||||||
</div>
|
</div>
|
||||||
<div className="select is-small is-rounded">
|
|
||||||
<select style={{ backgroundColor: '#F6F3F2', borderColor: 'transparent', color: '#1c1b1b', fontWeight: 500 }}>
|
|
||||||
<option>All Couriers</option>
|
|
||||||
<option>Delhivery Air</option>
|
|
||||||
<option>Blue Dart Express</option>
|
|
||||||
<option>Tradhox Air</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -326,159 +365,204 @@ export const OrdersPage: React.FC = () => {
|
||||||
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Order & Date</th>
|
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Order & Date</th>
|
||||||
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Customer & City</th>
|
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Customer & City</th>
|
||||||
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Handcrafted Item</th>
|
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Handcrafted Item</th>
|
||||||
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Amount & Payment</th>
|
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Amount</th>
|
||||||
|
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Carrier / AWB</th>
|
||||||
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Status</th>
|
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Status</th>
|
||||||
<th className="has-text-grey is-size-7 has-text-right" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Action</th>
|
<th className="has-text-grey is-size-7 has-text-right" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredOrders.map((ord) => (
|
{filteredOrders.length === 0 ? (
|
||||||
<tr key={ord.id} style={{ borderBottom: '1px solid var(--trad-border)' }}>
|
<tr>
|
||||||
<td className="is-vcentered">
|
<td colSpan={7} className="has-text-centered py-6 has-text-grey">No orders found in this section.</td>
|
||||||
<div className="has-text-weight-bold is-size-7" style={{ color: 'var(--trad-maroon)' }}>{ord.orderNumber}</div>
|
|
||||||
<div className="is-size-7 has-text-grey mt-1">{ord.date}</div>
|
|
||||||
</td>
|
|
||||||
<td className="is-vcentered">
|
|
||||||
<div className="has-text-weight-semibold is-size-7" style={{ color: '#1c1b1b' }}>{ord.customerName}</div>
|
|
||||||
<div className="is-size-7 has-text-grey mt-1">{ord.customerCity}, {ord.customerState}</div>
|
|
||||||
</td>
|
|
||||||
<td className="is-vcentered">
|
|
||||||
<div className="is-flex is-align-items-center gap-3">
|
|
||||||
<div style={{ width: '44px', height: '44px', borderRadius: '8px', backgroundColor: '#F6F3F2', flexShrink: 0, overflow: 'hidden' }}>
|
|
||||||
<img src={`https://images.unsplash.com/photo-1610701596007-11502861dcfa?auto=format&fit=crop&w=100&q=80`} alt={ord.productTitle} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="has-text-weight-semibold is-size-7" style={{ color: '#1c1b1b' }}>{ord.productTitle}</div>
|
|
||||||
<div className="is-size-7 has-text-grey mt-1">{ord.quantity} Unit(s)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="is-vcentered">
|
|
||||||
<div className="has-text-weight-semibold is-size-7" style={{ color: '#1c1b1b' }}>₹{ord.amount.toLocaleString()}</div>
|
|
||||||
<div className="is-size-7 mt-1 is-flex is-align-items-center" style={{ color: '#5dbe73' }}>
|
|
||||||
<CheckCircle2 size={13} className="mr-1" /> Prepaid
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="is-vcentered">
|
|
||||||
<span className={`tag is-rounded is-small has-text-weight-semibold ${
|
|
||||||
ord.status === 'new' ? 'is-warning is-light' :
|
|
||||||
ord.status === 'ready_to_ship' ? 'is-info is-light' :
|
|
||||||
ord.status === 'dispatched' ? 'is-primary is-light' : 'is-success is-light'
|
|
||||||
}`}>
|
|
||||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: 'currentColor', marginRight: '6px' }}></span>
|
|
||||||
{ord.status.replace('_', ' ').toUpperCase()}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="is-vcentered has-text-right">
|
|
||||||
<div className="is-flex is-align-items-center is-justify-content-flex-end gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (ord.status === 'ready_to_ship') setActiveOrder(ord);
|
|
||||||
else if (ord.status !== 'delivered' && ord.status !== 'dispatched') setSelectedOrderForLabel(ord);
|
|
||||||
}}
|
|
||||||
className="button is-small is-rounded is-trad-primary shadow-sm"
|
|
||||||
style={{ fontWeight: 600, fontSize: '0.75rem', height: '32px' }}
|
|
||||||
>
|
|
||||||
{ord.status === 'delivered' ? <span className="mr-1 material-symbols-outlined is-size-6" style={{ fontSize: '15px' }}>description</span> : <Printer size={14} className="mr-1" />}
|
|
||||||
{ord.status === 'delivered' ? 'Receipt' : ord.status === 'dispatched' ? 'Track' : ord.status === 'ready_to_ship' ? 'View Details' : 'Print Label'}
|
|
||||||
</button>
|
|
||||||
<button className="button is-small is-rounded is-white has-text-grey" style={{ width: '32px', height: '32px', padding: 0 }}>
|
|
||||||
<MoreVertical size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
) : (
|
||||||
|
filteredOrders.map((ord) => (
|
||||||
|
<tr key={ord.id} style={{ borderBottom: '1px solid var(--trad-border)' }}>
|
||||||
|
<td className="is-vcentered">
|
||||||
|
<div className="has-text-weight-bold is-size-7" style={{ color: 'var(--trad-maroon)' }}>{ord.orderNumber}</div>
|
||||||
|
<div className="is-size-7 has-text-grey mt-1">{ord.date}</div>
|
||||||
|
</td>
|
||||||
|
<td className="is-vcentered">
|
||||||
|
<div className="has-text-weight-semibold is-size-7" style={{ color: '#1c1b1b' }}>{ord.customerName}</div>
|
||||||
|
<div className="is-size-7 has-text-grey mt-1">{ord.customerCity}, {ord.customerState}</div>
|
||||||
|
</td>
|
||||||
|
<td className="is-vcentered">
|
||||||
|
<div className="has-text-weight-semibold is-size-7" style={{ color: '#1c1b1b' }}>{ord.productTitle}</div>
|
||||||
|
<div className="is-size-7 has-text-grey mt-1">{ord.quantity} Unit(s)</div>
|
||||||
|
</td>
|
||||||
|
<td className="is-vcentered">
|
||||||
|
<div className="has-text-weight-semibold is-size-7" style={{ color: '#1c1b1b' }}>₹{ord.amount.toLocaleString()}</div>
|
||||||
|
<div className="is-size-7 mt-1 is-flex is-align-items-center" style={{ color: '#5dbe73' }}>
|
||||||
|
<CheckCircle2 size={13} className="mr-1" /> COD / Prepaid
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="is-vcentered">
|
||||||
|
<div className="is-size-7 has-text-weight-bold">{ord.courierPartner || 'Shadowfax Logistics'}</div>
|
||||||
|
<div className="is-size-7 has-text-grey mt-1">{ord.awbNumber || 'Pending'}</div>
|
||||||
|
</td>
|
||||||
|
<td className="is-vcentered">
|
||||||
|
<span className={`tag is-rounded is-small has-text-weight-semibold ${
|
||||||
|
ord.status === 'new' ? 'is-warning is-light' :
|
||||||
|
ord.status === 'ready_to_ship' ? 'is-info is-light' :
|
||||||
|
ord.status === 'dispatched' ? 'is-primary is-light' : 'is-success is-light'
|
||||||
|
}`}>
|
||||||
|
{ord.status.replace('_', ' ').toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="is-vcentered has-text-right">
|
||||||
|
<div className="is-flex is-align-items-center is-justify-content-flex-end gap-2">
|
||||||
|
{ord.status === 'new' ? (
|
||||||
|
<button
|
||||||
|
onClick={() => handleAcceptAndShip(ord)}
|
||||||
|
className="button is-small is-rounded is-success"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<CheckCircle2 size={14} className="mr-1" /> Accept & Ship
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => handleTrackShipment(ord)}
|
||||||
|
className="button is-small is-rounded is-info is-light"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<Send size={14} className="mr-1" /> Track
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleFetchPrintLabel(ord)}
|
||||||
|
className="button is-small is-rounded is-trad-primary shadow-sm"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<Printer size={14} className="mr-1" /> Label
|
||||||
|
</button>
|
||||||
|
{ord.status === 'ready_to_ship' && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleCancelPickup(ord)}
|
||||||
|
className="button is-small is-rounded is-danger is-light"
|
||||||
|
title="Cancel Delivery Pickup"
|
||||||
|
>
|
||||||
|
<XCircle size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination Footer */}
|
|
||||||
<div className="p-4 is-flex is-flex-direction-column is-flex-direction-row-tablet is-align-items-center is-justify-content-space-between" style={{ backgroundColor: '#F6F3F2', borderTop: '1px solid var(--trad-border)' }}>
|
|
||||||
<span className="is-size-7 has-text-grey mb-3 mb-0-tablet">
|
|
||||||
Showing <strong style={{ color: '#1c1b1b' }}>{filteredOrders.length === 0 ? 0 : `1–${filteredOrders.length}`}</strong> of <strong style={{ color: '#1c1b1b' }}>{orders.length}</strong> orders
|
|
||||||
</span>
|
|
||||||
<div className="is-flex is-align-items-center gap-1">
|
|
||||||
<button className="button is-small is-rounded is-white has-text-grey" disabled>Previous</button>
|
|
||||||
<button className="button is-small is-rounded is-trad-primary has-text-weight-bold" style={{ width: '32px', height: '32px', padding: 0 }}>1</button>
|
|
||||||
<button className="button is-small is-rounded is-white has-text-grey" style={{ width: '32px', height: '32px', padding: 0 }}>2</button>
|
|
||||||
<button className="button is-small is-rounded is-white has-text-grey" style={{ width: '32px', height: '32px', padding: 0 }}>3</button>
|
|
||||||
<span className="is-size-7 has-text-grey px-1">...</span>
|
|
||||||
<button className="button is-small is-rounded is-white has-text-grey">Next</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* MODAL: Thermal Shipping Label View */}
|
||||||
|
|
||||||
{/* MODAL 1: 4x6 Thermal Shipping Label View (Faithful to Stitch) */}
|
|
||||||
{selectedOrderForLabel && (
|
{selectedOrderForLabel && (
|
||||||
<div className="modal is-active">
|
<div className="modal is-active">
|
||||||
<div className="modal-background" onClick={() => setSelectedOrderForLabel(null)}></div>
|
<div className="modal-background" onClick={() => setSelectedOrderForLabel(null)}></div>
|
||||||
<div className="modal-card" style={{ maxWidth: '440px' }}>
|
<div className="modal-card" style={{ maxWidth: '440px' }}>
|
||||||
<header className="modal-card-head" style={{ background: '#ffffff', borderBottom: '1px solid var(--trad-border)' }}>
|
<header className="modal-card-head" style={{ background: '#ffffff', borderBottom: '1px solid var(--trad-border)' }}>
|
||||||
<p className="modal-card-title is-size-6 has-text-weight-bold">
|
<p className="modal-card-title is-size-6 has-text-weight-bold">
|
||||||
Thermal 4x6 Shipping Label View
|
Print 4x6 Delivery Shipping Label
|
||||||
</p>
|
</p>
|
||||||
<button className="delete" aria-label="close" onClick={() => setSelectedOrderForLabel(null)}></button>
|
<button className="delete" aria-label="close" onClick={() => setSelectedOrderForLabel(null)}></button>
|
||||||
</header>
|
</header>
|
||||||
<section className="modal-card-body p-4 has-text-centered" style={{ background: '#F5F5F5' }}>
|
<section className="modal-card-body p-4 has-text-centered" style={{ background: '#F5F5F5' }}>
|
||||||
{/* 4x6 Label Container */}
|
|
||||||
<div className="thermal-label-container has-text-left" style={{ background: '#ffffff', border: '2px dashed #000', padding: '16px', borderRadius: '4px' }}>
|
<div className="thermal-label-container has-text-left" style={{ background: '#ffffff', border: '2px dashed #000', padding: '16px', borderRadius: '4px' }}>
|
||||||
<div className="is-flex is-justify-content-space-between is-align-items-center mb-2 pb-2" style={{ borderBottom: '2px solid #000' }}>
|
<div className="is-flex is-justify-content-space-between is-align-items-center mb-2 pb-2" style={{ borderBottom: '2px solid #000' }}>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="has-text-weight-black is-size-5 mb-0" style={{ letterSpacing: '1px' }}>TRADHOX EXPRESS</h4>
|
<h4 className="has-text-weight-black is-size-5 mb-0" style={{ letterSpacing: '1px' }}>{selectedOrderForLabel.carrier || 'SHADOWFAX LOGISTICS'}</h4>
|
||||||
<span className="is-size-7 font-weight-bold">PREPAID AIR LOGISTICS</span>
|
<span className="is-size-7 font-weight-bold">EXPRESS DELIVERY</span>
|
||||||
</div>
|
|
||||||
<div className="has-text-right">
|
|
||||||
<span className="has-text-weight-black is-size-5">DEL</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-2 has-text-centered py-2" style={{ borderBottom: '2px solid #000' }}>
|
<div className="mb-2 has-text-centered py-2" style={{ borderBottom: '2px solid #000' }}>
|
||||||
<div className="is-size-7 font-mono mb-1">AWB: {selectedOrderForLabel.awbNumber}</div>
|
<div className="is-size-7 font-mono mb-1">AWB: {selectedOrderForLabel.awb_number}</div>
|
||||||
{/* Barcode Simulation */}
|
{selectedOrderForLabel.barcode_url ? (
|
||||||
<div className="is-flex is-justify-content-center py-2" style={{ background: '#000', height: '38px', letterSpacing: '4px', color: '#fff', fontSize: '10px', alignItems: 'center' }}>
|
<img src={selectedOrderForLabel.barcode_url} alt="QR Code" style={{ width: '120px' }} className="mx-auto" />
|
||||||
||||||| | ||||| |||| || |||||| | ||||| ||||
|
) : (
|
||||||
</div>
|
<div className="is-flex is-justify-content-center py-2" style={{ background: '#000', height: '38px', letterSpacing: '4px', color: '#fff', fontSize: '10px', alignItems: 'center' }}>
|
||||||
<div className="is-size-7 font-mono mt-1">ORDER ID: {selectedOrderForLabel.orderNumber}</div>
|
||||||| | ||||| |||| || |||||| | ||||| ||||
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="is-size-7 font-mono mt-1">ORDER ID: {selectedOrderForLabel.order_id}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-2 pb-2" style={{ borderBottom: '1px solid #000', fontSize: '11px', lineHeight: 1.4 }}>
|
<div className="mb-2 pb-2" style={{ borderBottom: '1px solid #000', fontSize: '11px', lineHeight: 1.4 }}>
|
||||||
<div className="has-text-weight-bold mb-1">DELIVER TO:</div>
|
<div className="has-text-weight-bold mb-1">DELIVER TO:</div>
|
||||||
<div className="has-text-weight-black is-size-6">{selectedOrderForLabel.customerName}</div>
|
<div className="has-text-weight-black is-size-6">{selectedOrderForLabel.recipient?.name}</div>
|
||||||
<div>Flat 402, Heritage Residency, MG Road</div>
|
<div>{selectedOrderForLabel.recipient?.address}</div>
|
||||||
<div>{selectedOrderForLabel.customerCity}, {selectedOrderForLabel.customerState} - 400001</div>
|
<div>Phone: {selectedOrderForLabel.recipient?.phone}</div>
|
||||||
<div>Phone: +91 98200 XXXXX</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-2 pb-2" style={{ borderBottom: '1px solid #000', fontSize: '11px', lineHeight: 1.4 }}>
|
<div className="mb-2 pb-2" style={{ borderBottom: '1px solid #000', fontSize: '11px', lineHeight: 1.4 }}>
|
||||||
<div className="has-text-weight-bold mb-1">RETURN ADDRESS (SELLER GUILD):</div>
|
<div className="has-text-weight-bold mb-1">DISPATCHED FROM (SELLER):</div>
|
||||||
<div>Raghavendra Artisan Guild</div>
|
<div>{selectedOrderForLabel.sender?.name}</div>
|
||||||
<div>42, Shilpkala Marg, Sanganer Craft Cluster</div>
|
<div>{selectedOrderForLabel.sender?.address}</div>
|
||||||
<div>Jaipur, Rajasthan - 302029</div>
|
<div>Phone: {selectedOrderForLabel.sender?.phone}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="is-flex is-justify-content-space-between is-align-items-center" style={{ fontSize: '11px' }}>
|
<div className="is-flex is-justify-content-space-between is-align-items-center" style={{ fontSize: '11px' }}>
|
||||||
<div>
|
<div>Item: {selectedOrderForLabel.package_details?.item}</div>
|
||||||
<span>Dead Wt: 1.20 Kg</span> | <span>Qty: {selectedOrderForLabel.quantity}</span>
|
<div className="has-text-weight-bold">Collectible COD: ₹{selectedOrderForLabel.package_details?.collectible_amount}</div>
|
||||||
</div>
|
|
||||||
<div className="has-text-weight-bold">
|
|
||||||
GI TAG CRAFT
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<footer className="modal-card-foot" style={{ background: '#ffffff', borderTop: '1px solid var(--trad-border)', justifyContent: 'space-between' }}>
|
<footer className="modal-card-foot" style={{ background: '#ffffff', borderTop: '1px solid var(--trad-border)', justifyContent: 'space-between' }}>
|
||||||
<button className="button is-small is-rounded" onClick={() => setSelectedOrderForLabel(null)}>Close</button>
|
<button className="button is-small is-rounded" onClick={() => setSelectedOrderForLabel(null)}>Close</button>
|
||||||
<button className="button is-trad-primary is-small is-rounded" onClick={() => alert('Printing to thermal 4x6 label printer...')}>
|
<button className="button is-trad-primary is-small is-rounded" onClick={() => window.print()}>
|
||||||
<Printer size={14} className="mr-1" />
|
<Printer size={14} className="mr-1" />
|
||||||
<span>Print to Thermal Printer</span>
|
<span>Print Thermal Label</span>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* MODAL: Live Shipment Tracking */}
|
||||||
|
{trackingInfo && (
|
||||||
|
<div className="modal is-active">
|
||||||
|
<div className="modal-background" onClick={() => setTrackingInfo(null)}></div>
|
||||||
|
<div className="modal-card" style={{ maxWidth: '500px' }}>
|
||||||
|
<header className="modal-card-head" style={{ backgroundColor: '#fff' }}>
|
||||||
|
<p className="modal-card-title is-size-6 has-text-weight-bold">
|
||||||
|
Live Shipment Tracking (Order #{trackingOrderNumber})
|
||||||
|
</p>
|
||||||
|
<button className="delete" aria-label="close" onClick={() => setTrackingInfo(null)}></button>
|
||||||
|
</header>
|
||||||
|
<section className="modal-card-body">
|
||||||
|
<div className="content is-size-7">
|
||||||
|
<div className="p-3 mb-3" style={{ background: '#F6F3F2', borderRadius: '8px' }}>
|
||||||
|
<div><strong>AWB Number:</strong> {trackingInfo.awb_number}</div>
|
||||||
|
<div><strong>Current Status:</strong> <span className="tag is-info is-light">{trackingInfo.current_status || 'out_for_delivery'}</span></div>
|
||||||
|
{trackingInfo.rider_details && (
|
||||||
|
<div className="mt-2 pt-2" style={{ borderTop: '1px solid #ddd' }}>
|
||||||
|
<div><strong>Assigned Rider:</strong> {trackingInfo.rider_details.name}</div>
|
||||||
|
<div><strong>Rider Phone:</strong> {trackingInfo.rider_details.phone}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{trackingInfo.tracking_history && (
|
||||||
|
<div>
|
||||||
|
<h5 className="is-size-7 font-weight-bold mb-2">Tracking History Timeline:</h5>
|
||||||
|
<ul>
|
||||||
|
{trackingInfo.tracking_history.map((h: any, idx: number) => (
|
||||||
|
<li key={idx} className="mb-1">
|
||||||
|
<strong>{h.status}</strong> - {h.location || 'Hub'} ({h.timestamp})
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<footer className="modal-card-foot" style={{ backgroundColor: '#fff', justifyContent: 'flex-end' }}>
|
||||||
|
<button className="button is-small" onClick={() => setTrackingInfo(null)}>Close</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,104 @@
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useSeller } from '../../context/SellerContext';
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
Printer,
|
Printer,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
Send
|
Send,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
QrCode,
|
||||||
|
FileText,
|
||||||
|
AlertCircle
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
export const ReturnsTrackerPage: React.FC = () => {
|
export const ReturnsTrackerPage: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { returnsList, actionReturn, trackReturn, updateReturnQC, getReturnLabel } = useSeller();
|
||||||
|
|
||||||
const reverseShipments: any[] = [];
|
const [activeReturn, setActiveReturn] = useState<any | null>(null);
|
||||||
|
const [trackingInfo, setTrackingInfo] = useState<any | null>(null);
|
||||||
|
const [labelData, setLabelData] = useState<any | null>(null);
|
||||||
|
const [qcModalReturn, setQcModalReturn] = useState<any | null>(null);
|
||||||
|
|
||||||
|
const [qcSerialNumber, setQcSerialNumber] = useState('SN-9988112233');
|
||||||
|
const [qcBrandName, setQcBrandName] = useState('Authentic Artisan');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [notice, setNotice] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleApproveReturn = async (id: string | number) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setNotice(null);
|
||||||
|
try {
|
||||||
|
await actionReturn(String(id), 'Approved');
|
||||||
|
setNotice('Return approved successfully! Customer return pickup & reverse AWB generated.');
|
||||||
|
} catch (e: any) {
|
||||||
|
alert(`Failed to approve return: ${e.message}`);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRejectReturn = async (id: string | number) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setNotice(null);
|
||||||
|
try {
|
||||||
|
await actionReturn(String(id), 'Rejected');
|
||||||
|
setNotice('Return request rejected.');
|
||||||
|
} catch (e: any) {
|
||||||
|
alert(`Failed to reject return: ${e.message}`);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTrackReturn = async (returnObj: any) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setNotice(null);
|
||||||
|
try {
|
||||||
|
const res = await trackReturn(returnObj.return_id || String(returnObj.id));
|
||||||
|
setTrackingInfo(res.data || res);
|
||||||
|
setActiveReturn(returnObj);
|
||||||
|
} catch (e: any) {
|
||||||
|
alert(`Tracking failed: ${e.message}`);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrintLabel = async (returnObj: any) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await getReturnLabel(returnObj.return_id || String(returnObj.id));
|
||||||
|
setLabelData(res.label || res);
|
||||||
|
} catch (e: any) {
|
||||||
|
alert(`Failed to fetch label: ${e.message}`);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveQC = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!qcModalReturn) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await updateReturnQC(qcModalReturn.return_id || String(qcModalReturn.id), {
|
||||||
|
qc_check: true,
|
||||||
|
serial_number: qcSerialNumber,
|
||||||
|
brand_name: qcBrandName
|
||||||
|
});
|
||||||
|
setNotice('Doorstep Quality Check (QC) specifications updated for rider!');
|
||||||
|
setQcModalReturn(null);
|
||||||
|
} catch (e: any) {
|
||||||
|
alert(`Failed to update QC: ${e.message}`);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="returns-tracker-page" style={{ maxWidth: '1400px', margin: '0 auto' }}>
|
<div className="returns-tracker-page" style={{ maxWidth: '1400px', margin: '0 auto' }}>
|
||||||
|
|
@ -26,39 +112,34 @@ export const ReturnsTrackerPage: React.FC = () => {
|
||||||
</nav>
|
</nav>
|
||||||
<h1 className="title is-size-2 mb-2" style={{ letterSpacing: '-0.02em', fontWeight: 700, color: '#1c1b1b' }}>Return Order Tracker</h1>
|
<h1 className="title is-size-2 mb-2" style={{ letterSpacing: '-0.02em', fontWeight: 700, color: '#1c1b1b' }}>Return Order Tracker</h1>
|
||||||
<p className="is-size-6 has-text-grey" style={{ maxWidth: '800px', color: '#564243' }}>
|
<p className="is-size-6 has-text-grey" style={{ maxWidth: '800px', color: '#564243' }}>
|
||||||
Track reverse logistics sweeps, in-transit customer returns, unboxing quality checks, and 24-hour transit damage claims across artisan craft hubs.
|
Track reverse logistics sweeps, in-transit customer returns, doorstep Quality Checks (QC), and return delivery labels.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="is-flex is-align-items-center gap-3">
|
<div className="is-flex is-align-items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/dashboard/orders')} // Ideally goes to report damage view
|
onClick={() => navigate('/dashboard/orders')}
|
||||||
className="button is-rounded is-trad-primary shadow-sm"
|
className="button is-rounded is-trad-primary shadow-sm"
|
||||||
style={{ height: '44px', fontWeight: 600, fontSize: '0.9rem', padding: '0 1.5rem', backgroundColor: '#791b2c', color: '#fff' }}
|
style={{ height: '44px', fontWeight: 600, fontSize: '0.9rem', padding: '0 1.5rem', backgroundColor: '#791b2c', color: '#fff' }}
|
||||||
>
|
>
|
||||||
<ShieldAlert size={18} className="mr-2" />
|
<ShieldAlert size={18} className="mr-2" />
|
||||||
File Damage Claim
|
File Transit Damage Claim
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{notice && (
|
||||||
|
<div className="notification is-success is-light p-3 mb-4 is-size-7">
|
||||||
|
<span className="has-text-weight-bold">{notice}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Orders Table Container */}
|
{/* Orders Table Container */}
|
||||||
<div className="card" style={{ borderRadius: '16px', border: '1px solid var(--trad-border)', boxShadow: '0 2px 8px rgba(0,0,0,0.02)', overflow: 'hidden', backgroundColor: '#ffffff' }}>
|
<div className="card" style={{ borderRadius: '16px', border: '1px solid var(--trad-border)', boxShadow: '0 2px 8px rgba(0,0,0,0.02)', overflow: 'hidden', backgroundColor: '#ffffff' }}>
|
||||||
{/* Table Filters Header */}
|
{/* Table Filters Header */}
|
||||||
<div className="p-4 is-flex is-flex-wrap-wrap is-align-items-center is-justify-content-space-between" style={{ borderBottom: '1px solid var(--trad-border)', gap: '1rem' }}>
|
<div className="p-4 is-flex is-flex-wrap-wrap is-align-items-center is-justify-content-space-between" style={{ borderBottom: '1px solid var(--trad-border)', gap: '1rem' }}>
|
||||||
<div className="is-flex is-align-items-center" style={{ gap: '0.5rem' }}>
|
<div className="is-flex is-align-items-center" style={{ gap: '0.5rem' }}>
|
||||||
<span className="has-text-weight-bold" style={{ color: '#1c1b1b' }}>Reverse Shipments</span>
|
<span className="has-text-weight-bold" style={{ color: '#1c1b1b' }}>Reverse Shipments</span>
|
||||||
<span className="tag is-rounded" style={{ backgroundColor: '#f0eded', color: '#564243', fontWeight: 600 }}>{reverseShipments.length} Active</span>
|
<span className="tag is-rounded" style={{ backgroundColor: '#f0eded', color: '#564243', fontWeight: 600 }}>{returnsList.length} Active</span>
|
||||||
</div>
|
|
||||||
<div className="is-flex is-align-items-center gap-3">
|
|
||||||
<span className="is-size-7 has-text-grey">Scroll table</span>
|
|
||||||
<div className="is-flex gap-1">
|
|
||||||
<button className="button is-small is-rounded is-white" style={{ width: '32px', height: '32px', padding: 0, border: '1px solid #e5e2e1' }}>
|
|
||||||
<ChevronLeft size={16} />
|
|
||||||
</button>
|
|
||||||
<button className="button is-small is-rounded is-white" style={{ width: '32px', height: '32px', padding: 0, border: '1px solid #e5e2e1' }}>
|
|
||||||
<ChevronRight size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -67,82 +148,219 @@ export const ReturnsTrackerPage: React.FC = () => {
|
||||||
<table className="table is-fullwidth is-hoverable" style={{ borderCollapse: 'collapse' }}>
|
<table className="table is-fullwidth is-hoverable" style={{ borderCollapse: 'collapse' }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ backgroundColor: '#ffffff' }}>
|
<tr style={{ backgroundColor: '#ffffff' }}>
|
||||||
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Return<br/>& Date</th>
|
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Return ID & Order</th>
|
||||||
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Order & Craft Item</th>
|
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Item & Customer</th>
|
||||||
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Customer &<br/>City</th>
|
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Return Reason</th>
|
||||||
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Return<br/>Reason</th>
|
|
||||||
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Status</th>
|
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Status</th>
|
||||||
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Carrier /<br/>AWB</th>
|
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Reverse AWB</th>
|
||||||
<th className="has-text-grey is-size-7 pb-4 has-text-right" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Action</th>
|
<th className="has-text-grey is-size-7 pb-4 has-text-right" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{reverseShipments.map((rtn) => (
|
{returnsList.length === 0 ? (
|
||||||
<tr key={rtn.id} style={{ borderBottom: '1px solid #e5e2e1' }}>
|
<tr>
|
||||||
<td className="is-vcentered py-4">
|
<td colSpan={6} className="has-text-centered py-6 has-text-grey">
|
||||||
<div className="has-text-weight-bold is-size-7" style={{ color: '#5a0118' }}>{rtn.id}</div>
|
<AlertCircle size={32} className="mx-auto mb-2" />
|
||||||
<div className="is-size-7 mt-1" style={{ color: '#564243' }}>{rtn.date}</div>
|
<div>No active return requests found.</div>
|
||||||
</td>
|
|
||||||
<td className="is-vcentered py-4">
|
|
||||||
<div className="is-flex is-align-items-center gap-3">
|
|
||||||
<div style={{ width: '40px', height: '40px', borderRadius: '50%', backgroundColor: '#F6F3F2', flexShrink: 0, overflow: 'hidden' }}>
|
|
||||||
<img src={rtn.imageUrl} alt={rtn.title} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="has-text-weight-semibold is-size-7" style={{ color: '#1c1b1b' }}>{rtn.title}</div>
|
|
||||||
<div className="is-size-7 mt-1" style={{ color: '#564243' }}>{rtn.sku} · {rtn.quantity} {rtn.quantity === 1 ? 'Unit' : 'Units'} (₹{rtn.amount.toLocaleString()})</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="is-vcentered py-4">
|
|
||||||
<div className="is-size-7" style={{ color: '#1c1b1b' }}>{rtn.customerName}</div>
|
|
||||||
<div className="is-size-7 mt-1 is-flex is-align-items-center" style={{ color: '#564243' }}>
|
|
||||||
<span className="material-symbols-outlined mr-1" style={{ fontSize: '14px' }}>location_on</span>
|
|
||||||
{rtn.customerCity} ({rtn.customerPin})
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="is-vcentered py-4">
|
|
||||||
<div className="is-size-7 is-flex is-align-items-center" style={{ color: rtn.isDamage ? '#ba1a1a' : '#1c1b1b' }}>
|
|
||||||
{rtn.isDamage && <span className="material-symbols-outlined mr-1" style={{ fontSize: '14px' }}>warning</span>}
|
|
||||||
{rtn.returnReason}
|
|
||||||
</div>
|
|
||||||
<div className="is-size-7 mt-1" style={{ color: rtn.reasonColor || '#564243' }}>{rtn.reasonSub}</div>
|
|
||||||
</td>
|
|
||||||
<td className="is-vcentered py-4">
|
|
||||||
<span className={`tag is-rounded has-text-weight-semibold ${rtn.statusColor}`} style={{ padding: '0 12px', height: '32px', fontSize: '12px' }}>
|
|
||||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: 'currentColor', marginRight: '6px' }}></span>
|
|
||||||
{rtn.status}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="is-vcentered py-4">
|
|
||||||
<div className="is-size-7" style={{ color: '#1c1b1b' }}>{rtn.carrier}</div>
|
|
||||||
<div className="is-size-7 mt-1" style={{ color: '#564243' }}>{rtn.awb}</div>
|
|
||||||
</td>
|
|
||||||
<td className="is-vcentered py-4 has-text-right">
|
|
||||||
<button className="button is-small is-rounded is-light px-4" style={{ height: '36px', fontWeight: 600, backgroundColor: '#f6f3f2', border: 'none', color: '#1c1b1b' }}>
|
|
||||||
<Send size={14} className="mr-2" />
|
|
||||||
Track
|
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
) : (
|
||||||
|
returnsList.map((rtn) => (
|
||||||
|
<tr key={rtn.id} style={{ borderBottom: '1px solid #e5e2e1' }}>
|
||||||
|
<td className="is-vcentered py-4">
|
||||||
|
<div className="has-text-weight-bold is-size-7" style={{ color: '#5a0118' }}>{rtn.return_id || `RET-${rtn.id}`}</div>
|
||||||
|
<div className="is-size-7 mt-1" style={{ color: '#564243' }}>Order: {rtn.order_id}</div>
|
||||||
|
</td>
|
||||||
|
<td className="is-vcentered py-4">
|
||||||
|
<div className="has-text-weight-semibold is-size-7" style={{ color: '#1c1b1b' }}>{rtn.item}</div>
|
||||||
|
<div className="is-size-7 mt-1" style={{ color: '#564243' }}>Customer: {rtn.customer}</div>
|
||||||
|
</td>
|
||||||
|
<td className="is-vcentered py-4">
|
||||||
|
<div className="is-size-7" style={{ color: '#1c1b1b' }}>{rtn.reason}</div>
|
||||||
|
</td>
|
||||||
|
<td className="is-vcentered py-4">
|
||||||
|
<span className={`tag is-rounded has-text-weight-semibold ${
|
||||||
|
rtn.status === 'Approved' || rtn.status === 'In Transit' ? 'is-info is-light' :
|
||||||
|
rtn.status === 'Rejected' ? 'is-danger is-light' : 'is-warning is-light'
|
||||||
|
}`} style={{ padding: '0 12px', height: '28px', fontSize: '12px' }}>
|
||||||
|
{rtn.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="is-vcentered py-4">
|
||||||
|
<div className="is-size-7 has-text-weight-bold" style={{ color: '#1c1b1b' }}>{rtn.returning_tracking || 'Pending Pickup'}</div>
|
||||||
|
<div className="is-size-7 mt-1" style={{ color: '#564243' }}>Carrier: Shadowfax</div>
|
||||||
|
</td>
|
||||||
|
<td className="is-vcentered py-4 has-text-right">
|
||||||
|
<div className="is-flex is-align-items-center is-justify-content-flex-end gap-2">
|
||||||
|
{rtn.status === 'Pending Approval' ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => handleApproveReturn(rtn.id)}
|
||||||
|
className="button is-small is-success is-rounded"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<CheckCircle2 size={14} className="mr-1" /> Approve
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRejectReturn(rtn.id)}
|
||||||
|
className="button is-small is-danger is-light is-rounded"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<XCircle size={14} className="mr-1" /> Reject
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => setQcModalReturn(rtn)}
|
||||||
|
className="button is-small is-light is-rounded"
|
||||||
|
title="Update Doorstep QC"
|
||||||
|
>
|
||||||
|
QC Specs
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleTrackReturn(rtn)}
|
||||||
|
className="button is-small is-info is-light is-rounded"
|
||||||
|
>
|
||||||
|
<Send size={14} className="mr-1" /> Track
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handlePrintLabel(rtn)}
|
||||||
|
className="button is-small is-trad-primary is-rounded"
|
||||||
|
>
|
||||||
|
<Printer size={14} className="mr-1" /> Label
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Pagination Footer */}
|
{/* QC Modal */}
|
||||||
<div className="p-4 is-flex is-align-items-center is-justify-content-space-between" style={{ backgroundColor: '#fcf9f8', borderTop: '1px solid #e5e2e1' }}>
|
{qcModalReturn && (
|
||||||
<span className="is-size-7" style={{ color: '#564243' }}>
|
<div className="modal is-active">
|
||||||
Showing {reverseShipments.length} of {reverseShipments.length} active reverse shipments
|
<div className="modal-background" onClick={() => setQcModalReturn(null)}></div>
|
||||||
</span>
|
<div className="modal-card" style={{ maxWidth: '500px' }}>
|
||||||
<div className="is-flex is-align-items-center gap-1">
|
<header className="modal-card-head" style={{ backgroundColor: '#fff' }}>
|
||||||
<button className="button is-small is-rounded is-white" style={{ border: 'none', color: '#564243', fontWeight: 500, backgroundColor: 'transparent' }}>Previous</button>
|
<p className="modal-card-title is-size-6 has-text-weight-bold">
|
||||||
<button className="button is-small is-rounded is-trad-primary has-text-weight-bold" style={{ width: '32px', height: '32px', padding: 0, backgroundColor: '#791b2c', color: '#fff', border: 'none' }}>1</button>
|
Configure Doorstep Quality Check (QC)
|
||||||
<button className="button is-small is-rounded is-white" style={{ width: '32px', height: '32px', padding: 0, border: 'none', color: '#564243', backgroundColor: 'transparent' }}>2</button>
|
</p>
|
||||||
<button className="button is-small is-rounded is-white" style={{ border: 'none', color: '#564243', fontWeight: 500, backgroundColor: 'transparent' }}>Next</button>
|
<button className="delete" aria-label="close" onClick={() => setQcModalReturn(null)}></button>
|
||||||
|
</header>
|
||||||
|
<section className="modal-card-body">
|
||||||
|
<form onSubmit={handleSaveQC}>
|
||||||
|
<div className="field mb-3">
|
||||||
|
<label className="label is-size-7">Return ID</label>
|
||||||
|
<input className="input is-small" value={qcModalReturn.return_id || qcModalReturn.id} disabled />
|
||||||
|
</div>
|
||||||
|
<div className="field mb-3">
|
||||||
|
<label className="label is-size-7">Item Name</label>
|
||||||
|
<input className="input is-small" value={qcModalReturn.item} disabled />
|
||||||
|
</div>
|
||||||
|
<div className="field mb-3">
|
||||||
|
<label className="label is-size-7">Brand Name for Verification</label>
|
||||||
|
<input
|
||||||
|
className="input is-small"
|
||||||
|
value={qcBrandName}
|
||||||
|
onChange={e => setQcBrandName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="field mb-4">
|
||||||
|
<label className="label is-size-7">Mandatory Serial / IMEI / Craft Tag Number</label>
|
||||||
|
<input
|
||||||
|
className="input is-small"
|
||||||
|
value={qcSerialNumber}
|
||||||
|
onChange={e => setQcSerialNumber(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="notification is-warning is-light is-size-7">
|
||||||
|
Rider will verify physical condition, brand label, and serial number at doorstep before accepting return package.
|
||||||
|
</div>
|
||||||
|
<div className="buttons is-right">
|
||||||
|
<button type="button" className="button is-small" onClick={() => setQcModalReturn(null)}>Cancel</button>
|
||||||
|
<button type="submit" className={`button is-small is-trad-primary ${isLoading ? 'is-loading' : ''}`}>Save QC Specs</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{/* Tracking Modal */}
|
||||||
|
{trackingInfo && (
|
||||||
|
<div className="modal is-active">
|
||||||
|
<div className="modal-background" onClick={() => setTrackingInfo(null)}></div>
|
||||||
|
<div className="modal-card" style={{ maxWidth: '500px' }}>
|
||||||
|
<header className="modal-card-head" style={{ backgroundColor: '#fff' }}>
|
||||||
|
<p className="modal-card-title is-size-6 has-text-weight-bold">
|
||||||
|
Live Return Shipment Tracking (Shadowfax V4)
|
||||||
|
</p>
|
||||||
|
<button className="delete" aria-label="close" onClick={() => setTrackingInfo(null)}></button>
|
||||||
|
</header>
|
||||||
|
<section className="modal-card-body">
|
||||||
|
<div className="content is-size-7">
|
||||||
|
<div className="p-3 mb-3" style={{ background: '#F6F3F2', borderRadius: '8px' }}>
|
||||||
|
<div><strong>Return ID:</strong> {trackingInfo.client_request_id || activeReturn?.return_id}</div>
|
||||||
|
<div><strong>Status:</strong> {trackingInfo.current_status || 'picked_up'}</div>
|
||||||
|
<div><strong>Doorstep QC Status:</strong> <span className="tag is-success is-light">{trackingInfo.qc_status || 'PASSED'}</span></div>
|
||||||
|
{trackingInfo.rider_details && (
|
||||||
|
<div className="mt-2 pt-2" style={{ borderTop: '1px solid #ddd' }}>
|
||||||
|
<div><strong>Rider Name:</strong> {trackingInfo.rider_details.name}</div>
|
||||||
|
<div><strong>Contact:</strong> {trackingInfo.rider_details.phone}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<footer className="modal-card-foot" style={{ backgroundColor: '#fff', justifyContent: 'flex-end' }}>
|
||||||
|
<button className="button is-small" onClick={() => setTrackingInfo(null)}>Close</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Return Label Modal */}
|
||||||
|
{labelData && (
|
||||||
|
<div className="modal is-active">
|
||||||
|
<div className="modal-background" onClick={() => setLabelData(null)}></div>
|
||||||
|
<div className="modal-card" style={{ maxWidth: '440px' }}>
|
||||||
|
<header className="modal-card-head" style={{ backgroundColor: '#fff' }}>
|
||||||
|
<p className="modal-card-title is-size-6 has-text-weight-bold">
|
||||||
|
Print Return Pickup Label
|
||||||
|
</p>
|
||||||
|
<button className="delete" aria-label="close" onClick={() => setLabelData(null)}></button>
|
||||||
|
</header>
|
||||||
|
<section className="modal-card-body p-4">
|
||||||
|
<div className="p-4" style={{ background: '#fff', border: '2px dashed #000', borderRadius: '4px' }}>
|
||||||
|
<div className="has-text-weight-bold is-size-6 mb-1">{labelData.title || 'CUSTOMER RETURN PICKUP LABEL'}</div>
|
||||||
|
<div className="is-size-7 mb-2">Carrier: {labelData.carrier} | AWB: {labelData.awb_number}</div>
|
||||||
|
<div className="is-size-7 mb-2"><strong>Return ID:</strong> {labelData.return_id}</div>
|
||||||
|
<div className="is-size-7 mb-2"><strong>Pickup From:</strong> {labelData.pickup_from?.name} ({labelData.pickup_from?.phone})</div>
|
||||||
|
<div className="is-size-7 mb-2"><strong>Return To:</strong> {labelData.return_to?.name}</div>
|
||||||
|
{labelData.barcode_url && (
|
||||||
|
<div className="has-text-centered my-3">
|
||||||
|
<img src={labelData.barcode_url} alt="QR Barcode" style={{ width: '120px' }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<footer className="modal-card-foot" style={{ backgroundColor: '#fff', justifyContent: 'space-between' }}>
|
||||||
|
<button className="button is-small" onClick={() => setLabelData(null)}>Close</button>
|
||||||
|
<button className="button is-small is-trad-primary" onClick={() => window.print()}>
|
||||||
|
<Printer size={14} className="mr-1" /> Print Label
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,22 @@
|
||||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://betapartner-api.tradhox.com/api';
|
const BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api';
|
||||||
|
|
||||||
const getAuthHeaders = () => {
|
const getAuthHeaders = (isMultipart = false) => {
|
||||||
const userStr = localStorage.getItem('tradhox_user_auth');
|
const userStr = localStorage.getItem('tradhox_user_auth');
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
if (userStr) {
|
if (userStr) {
|
||||||
const auth = JSON.parse(userStr);
|
try {
|
||||||
return {
|
const auth = JSON.parse(userStr);
|
||||||
'Authorization': `Bearer ${auth.access_token}`,
|
if (auth.access_token) {
|
||||||
'Content-Type': 'application/json'
|
headers['Authorization'] = `Bearer ${auth.access_token}`;
|
||||||
};
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse auth token:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { 'Content-Type': 'application/json' };
|
if (!isMultipart) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
|
|
@ -84,7 +91,7 @@ export const api = {
|
||||||
if (!res.ok) throw new Error('Logout failed');
|
if (!res.ok) throw new Error('Logout failed');
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
verifyOtp: async (type: string, otp: string) => {
|
verifyOtp: async (type: string, otp: string) => {
|
||||||
const res = await fetch(`${BASE_URL}/auth/verify-otp/`, {
|
const res = await fetch(`${BASE_URL}/auth/verify-otp/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -98,7 +105,8 @@ export const api = {
|
||||||
}
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Profile & Settings
|
||||||
getProfile: async () => {
|
getProfile: async () => {
|
||||||
const res = await fetch(`${BASE_URL}/profile/`, {
|
const res = await fetch(`${BASE_URL}/profile/`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|
@ -107,8 +115,7 @@ export const api = {
|
||||||
if (!res.ok) throw new Error('Failed to fetch profile');
|
if (!res.ok) throw new Error('Failed to fetch profile');
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
// Profile
|
|
||||||
updateProfile: async (profileData: any) => {
|
updateProfile: async (profileData: any) => {
|
||||||
const res = await fetch(`${BASE_URL}/profile/`, {
|
const res = await fetch(`${BASE_URL}/profile/`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|
@ -118,14 +125,24 @@ export const api = {
|
||||||
if (!res.ok) throw new Error('Failed to update profile');
|
if (!res.ok) throw new Error('Failed to update profile');
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
submitGstin: async (gstin: string) => {
|
||||||
|
const res = await fetch(`${BASE_URL}/profile/submit-gstin/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify({ gstin })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to verify GSTIN');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
// Products
|
// Products
|
||||||
getProducts: async () => {
|
getProducts: async () => {
|
||||||
const res = await fetch(`${BASE_URL}/products/`, { headers: getAuthHeaders() });
|
const res = await fetch(`${BASE_URL}/products/`, { headers: getAuthHeaders() });
|
||||||
if (!res.ok) throw new Error('Failed to fetch products');
|
if (!res.ok) throw new Error('Failed to fetch products');
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
createProduct: async (productData: any) => {
|
createProduct: async (productData: any) => {
|
||||||
const res = await fetch(`${BASE_URL}/products/`, {
|
const res = await fetch(`${BASE_URL}/products/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -136,6 +153,16 @@ export const api = {
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateProduct: async (id: string | number, productData: any) => {
|
||||||
|
const res = await fetch(`${BASE_URL}/products/${id}/`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify(productData)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to update product');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
deleteProduct: async (id: string | number) => {
|
deleteProduct: async (id: string | number) => {
|
||||||
const res = await fetch(`${BASE_URL}/products/${id}/`, {
|
const res = await fetch(`${BASE_URL}/products/${id}/`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
|
@ -145,13 +172,27 @@ export const api = {
|
||||||
return res.ok;
|
return res.ok;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Orders
|
bulkUploadProducts: async (data: FormData | { products: any[] }) => {
|
||||||
|
const isFormData = data instanceof FormData;
|
||||||
|
const res = await fetch(`${BASE_URL}/products/bulk-upload/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(isFormData),
|
||||||
|
body: isFormData ? data : JSON.stringify(data)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const errData = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(errData.error || errData.message || 'Bulk upload failed');
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Orders & Forward Shipping
|
||||||
getOrders: async () => {
|
getOrders: async () => {
|
||||||
const res = await fetch(`${BASE_URL}/orders/`, { headers: getAuthHeaders() });
|
const res = await fetch(`${BASE_URL}/orders/`, { headers: getAuthHeaders() });
|
||||||
if (!res.ok) throw new Error('Failed to fetch orders');
|
if (!res.ok) throw new Error('Failed to fetch orders');
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
acceptOrder: async (id: string | number) => {
|
acceptOrder: async (id: string | number) => {
|
||||||
const res = await fetch(`${BASE_URL}/orders/${id}/accept/`, {
|
const res = await fetch(`${BASE_URL}/orders/${id}/accept/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -160,11 +201,144 @@ export const api = {
|
||||||
if (!res.ok) throw new Error('Failed to accept order');
|
if (!res.ok) throw new Error('Failed to accept order');
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
rejectOrder: async (id: string | number) => {
|
||||||
|
const res = await fetch(`${BASE_URL}/orders/${id}/reject/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to reject order');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
createOrderPickup: async (id: string | number, details?: any) => {
|
||||||
|
const res = await fetch(`${BASE_URL}/orders/${id}/create_pickup/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify(details || {})
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to create pickup');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
trackOrder: async (id: string | number, awb?: string) => {
|
||||||
|
const query = awb ? `?awb=${awb}` : '';
|
||||||
|
const res = await fetch(`${BASE_URL}/orders/${id}/track/${query}`, {
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to track shipment');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelOrderPickup: async (id: string | number, reason = 'Seller requested cancellation') => {
|
||||||
|
const res = await fetch(`${BASE_URL}/orders/${id}/cancel_pickup/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify({ reason })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to cancel pickup');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
getOrderLabel: async (id: string | number) => {
|
||||||
|
const res = await fetch(`${BASE_URL}/orders/${id}/print_label/`, {
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch delivery label');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Returns & Reverse Shipping with QC
|
||||||
|
getReturns: async () => {
|
||||||
|
const res = await fetch(`${BASE_URL}/returns/`, { headers: getAuthHeaders() });
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch return requests');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
actionReturn: async (id: string | number, action: 'Approved' | 'Rejected') => {
|
||||||
|
const res = await fetch(`${BASE_URL}/returns/${id}/action/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify({ action })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to process return request');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
createReturnPickup: async (id: string | number, details?: any) => {
|
||||||
|
const res = await fetch(`${BASE_URL}/returns/${id}/create_return_pickup/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify(details || {})
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to create return pickup');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
trackReturn: async (id: string | number) => {
|
||||||
|
const res = await fetch(`${BASE_URL}/returns/${id}/track/`, {
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to track return pickup');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateReturnQC: async (id: string | number, qcDetails: { qc_check?: boolean; serial_number?: string; brand_name?: string; item_name?: string }) => {
|
||||||
|
const res = await fetch(`${BASE_URL}/returns/${id}/update_qc/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify(qcDetails)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to update return QC');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
getReturnLabel: async (id: string | number) => {
|
||||||
|
const res = await fetch(`${BASE_URL}/returns/${id}/print_label/`, {
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch return label');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Serviceability
|
||||||
|
checkForwardServiceability: async (pincodes: string) => {
|
||||||
|
const res = await fetch(`${BASE_URL}/shipping/forward/serviceability/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify({ pincodes })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to check serviceability');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
checkReverseServiceability: async (pincodes: string) => {
|
||||||
|
const res = await fetch(`${BASE_URL}/shipping/reverse/serviceability/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify({ pincodes })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to check reverse serviceability');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
// Wallet
|
// Wallet
|
||||||
getWallet: async () => {
|
getWallet: async () => {
|
||||||
const res = await fetch(`${BASE_URL}/wallet/`, { headers: getAuthHeaders() });
|
const res = await fetch(`${BASE_URL}/wallet/`, { headers: getAuthHeaders() });
|
||||||
if (!res.ok) throw new Error('Failed to fetch wallet');
|
if (!res.ok) throw new Error('Failed to fetch wallet');
|
||||||
return res.json();
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
withdrawWallet: async (amount: number | string) => {
|
||||||
|
const res = await fetch(`${BASE_URL}/wallet/withdraw/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify({ amount: String(amount) })
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const errData = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(errData.error || errData.message || 'Withdrawal failed');
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue