feat: add shipping service documentation and update seller dashboard pages and api services
All checks were successful
Beta CI/CD Pipeline / build-and-test (push) Successful in 1m40s
Beta CI/CD Pipeline / deploy-beta (push) Successful in 45s

This commit is contained in:
vickytechkey 2026-09-25 22:22:00 +05:30
parent de8873859e
commit 42816905c4
12 changed files with 7469 additions and 321 deletions

View 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.

View 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`.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View 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.

View file

@ -0,0 +1,530 @@
# Tradhox Shipping Microservice (`shippingservice`)
[![Deploy Beta](https://img.shields.io/badge/Deployment-Beta_EC2-blue.svg)](https://beta-shipping.tradhox.com)
[![API Version](https://img.shields.io/badge/SFX_API-v1%20%7C%20v3%20%7C%20v4-brightgreen.svg)](#)
[![Database](https://img.shields.io/badge/PostgreSQL-16-blue.svg)](#)
## 📌 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`.

View file

@ -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}

View file

@ -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 ── */

View file

@ -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">

View file

@ -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>
); );
}; };

View file

@ -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>
); );
}; };

View file

@ -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();
} }
}; };