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>;
logout: () => void;
// Onboarding (Keeping as is for UI structure)
// Onboarding
onboarding: OnboardingData;
updateOnboarding: (data: Partial<OnboardingData>) => void;
onboardingStep: number;
@ -18,21 +18,36 @@ interface SellerContextType {
applicationReferenceId: string;
submitApplication: () => void;
// Inventory
// Inventory & Products
inventory: InventoryItem[];
fetchData: () => Promise<void>;
updateStock: (id: string, newStock: number) => void;
addProduct: (product: Omit<InventoryItem, 'id' | 'salesCount'>) => Promise<void>;
deleteProduct: (id: string) => Promise<void>;
// Orders
// Orders & Shipping
orders: OrderItem[];
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;
// 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[];
walletBalance: number;
nextPayoutDate: string;
withdrawWallet: (amount: number | string) => Promise<any>;
}
const defaultOnboarding: OnboardingData = {
@ -93,6 +108,7 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
const [applicationReferenceId] = useState<string>('');
const [inventory, setInventory] = useState<InventoryItem[]>([]);
const [orders, setOrders] = useState<OrderItem[]>([]);
const [returnsList, setReturnsList] = useState<any[]>([]);
const [payouts, setPayouts] = useState<PayoutTransaction[]>([]);
const [walletBalance, setWalletBalance] = useState<number>(0);
const [nextPayoutDate] = useState<string>('');
@ -105,13 +121,26 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
localStorage.setItem('tradhox_onboarding_step', onboardingStep.toString());
}, [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 () => {
if (!user) return;
try {
const [backendProducts, backendOrders, backendWallet] = await Promise.all([
api.getProducts().catch(() => []),
api.getOrders().catch(() => []),
api.getWallet().catch(() => ({}))
api.getWallet().catch(() => ({})),
fetchReturns()
]);
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}`,
date: o.date || 'Today',
customerName: (o.customer && typeof o.customer === 'string') ? (o.customer.split(' ')[0] || o.customer) : 'Customer',
customerCity: 'Customer City',
customerState: 'State',
customerCity: 'Bengaluru',
customerState: 'Karnataka',
productTitle: o.item || 'Order Item',
quantity: o.quantity || 1,
amount: parseFloat(o.total) || 0,
status: mapBackendStatusToFrontend(o.status),
courierPartner: o.carrier || 'Pending',
courierPartner: o.carrier || 'Shadowfax Logistics',
awbNumber: o.tracking || 'N/A',
pickupSlot: 'Not Scheduled',
deliveryDate: o.eta || 'N/A'
pickupSlot: 'Morning Slot (10 AM - 1 PM)',
deliveryDate: o.eta || '2-3 Days'
})));
// Map Wallet
@ -169,7 +198,7 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
} catch (e) {
console.error('Failed to fetch user data', e);
}
}, [user]);
}, [user, fetchReturns]);
useEffect(() => {
if (user) {
@ -335,11 +364,11 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
setInventory([]);
setOrders([]);
setReturnsList([]);
setPayouts([]);
};
const updateStock = (id: string, newStock: number) => {
// Optimistic UI update - ideally we'd also sync this to backend
setInventory(prev =>
prev.map(item =>
item.id === id
@ -355,7 +384,7 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
const addProduct = async (product: Omit<InventoryItem, 'id' | 'salesCount'>) => {
try {
const res = await api.createProduct({
await api.createProduct({
title: product.title,
sku: product.sku,
category: product.category,
@ -363,9 +392,10 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
stock: product.stock,
image: product.image
});
fetchData(); // Refresh list after create
fetchData();
} catch (e) {
console.error('Failed to add product', e);
throw e;
}
};
@ -375,19 +405,40 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
fetchData();
} catch (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']) => {
if (status === 'ready_to_ship') {
try {
await api.acceptOrder(orderId);
fetchData();
} catch (e) {
console.error('Failed to update order status', e);
}
await acceptOrder(orderId);
} else {
// Optimitistic update for unsupported backend mutations in this minimal API
setOrders(prev =>
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 (
<SellerContext.Provider
value={{
@ -415,15 +492,28 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
applicationReferenceId,
submitApplication,
inventory,
fetchData,
updateStock,
addProduct,
deleteProduct,
orders,
updateOrderStatus,
acceptOrder,
rejectOrder,
trackOrder,
cancelOrderPickup,
getOrderLabel,
reschedulePickup,
returnsList,
fetchReturns,
actionReturn,
trackReturn,
updateReturnQC,
getReturnLabel,
payouts,
walletBalance,
nextPayoutDate
nextPayoutDate,
withdrawWallet
}}
>
{children}

View file

@ -114,20 +114,24 @@ export const AddProductPage: React.FC = () => {
setTimeout(() => setAutoSaved(true), 600);
};
const handleFinish = () => {
addProduct({
sku: sku || `TRDX-${Date.now().toString().slice(-5)}`,
title: title || 'Authentic Handcrafted Item',
category: category || 'Handicrafts & Art',
craftTechnique: craftType || craftTechnique || 'Traditional',
giTagged,
price: Number(price) || 1000,
mrp: Number(mrp) || 1500,
stock: madeToOrder ? 0 : (Number(stock) || 10),
status: madeToOrder ? 'active' : (Number(stock) === 0 ? 'out_of_stock' : Number(stock) < 5 ? 'low_stock' : 'active'),
image: image || '/terracotta_vases.jpg',
});
navigate('/dashboard/inventory');
const handleFinish = async () => {
try {
await addProduct({
sku: sku || `TRDX-${Date.now().toString().slice(-5)}`,
title: title || 'Authentic Handcrafted Item',
category: category || 'Handicrafts & Art',
craftTechnique: craftType || craftTechnique || 'Traditional',
giTagged,
price: Number(price) || 1000,
mrp: Number(mrp) || 1500,
stock: madeToOrder ? 0 : (Number(stock) || 10),
status: madeToOrder ? 'active' : (Number(stock) === 0 ? 'out_of_stock' : Number(stock) < 5 ? 'low_stock' : 'active'),
image: image || '/terracotta_vases.jpg',
});
navigate('/dashboard/inventory');
} catch (e) {
console.error('Failed to add product:', e);
}
};
/* ── Field label style ── */

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useRef } from 'react';
import {
UploadCloud,
FileSpreadsheet,
@ -7,39 +7,86 @@ import {
AlertTriangle,
FileText
} 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 = () => {
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',
fileName: 'Jaipur_Blue_Pottery_SKUs_v2.xlsx',
fileName: 'Jaipur_Blue_Pottery_SKUs_v2.csv',
date: '12 Sep 2026, 16:40',
totalRows: 24,
successfulRows: 24,
failedRows: 0,
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);
setUploadSuccess(false);
setTimeout(() => {
setUploadSuccess(null);
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);
setUploadSuccess(true);
}, 1200);
if (fileInputRef.current) fileInputRef.current.value = '';
}
};
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 (
@ -52,17 +99,25 @@ export const BulkUploadPage: React.FC = () => {
</div>
<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="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>
<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>
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
accept=".csv,.xlsx,.zip"
onChange={handleFileChange}
/>
<div
className="p-6 has-text-centered is-clickable"
onClick={handleSimulateUpload}
onClick={() => fileInputRef.current?.click()}
style={{
border: '2px dashed var(--trad-maroon)',
borderRadius: '16px',
@ -72,10 +127,10 @@ export const BulkUploadPage: React.FC = () => {
>
<UploadCloud size={48} color="var(--trad-maroon)" className="mb-3 mx-auto" />
<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>
<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>
<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">
<span className="is-flex is-align-items-center font-weight-bold">
<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>
</div>
)}
@ -105,23 +169,6 @@ export const BulkUploadPage: React.FC = () => {
Use our standardized format to avoid inventory rejection or incorrect GST classification.
</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="is-flex is-align-items-center">
<FileText size={24} className="mr-3 has-text-info" />
@ -131,21 +178,21 @@ export const BulkUploadPage: React.FC = () => {
</div>
</div>
<button
onClick={() => alert('Downloaded Craft_Standard_Catalog.csv')}
type="button"
onClick={handleDownloadSampleCsv}
className="button is-small is-light"
>
<Download size={14} className="mr-1" />
<span>Download</span>
<span>Download CSV</span>
</button>
</div>
<div className="content is-size-7 has-text-grey">
<div className="has-text-weight-bold mb-1">Mandatory Fields in Spreadsheet:</div>
<ul className="pl-3" style={{ lineHeight: 1.6 }}>
<li>Unique SKU Code (e.g. TRAD-YOURNAME-001)</li>
<li>Product Title & Craft Technique</li>
<li>Selling Price (INR) & MRP</li>
<li>Package Dimensions (Length, Width, Height in cm, Weight in grams)</li>
<li>Title & SKU (e.g. TRAD-YOURNAME-001)</li>
<li>Category & Description</li>
<li>Price (INR) & Stock Quantity</li>
</ul>
</div>
</div>
@ -155,7 +202,7 @@ export const BulkUploadPage: React.FC = () => {
{/* Batch Processing History Table */}
<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)' }}>
<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 className="table-container mb-0">
<table className="table is-fullwidth trad-table">

View file

@ -15,14 +15,15 @@ import {
Truck,
CheckCircle2,
Inbox,
PhoneCall,
ArrowRight,
RefreshCw
RefreshCw,
Send,
XCircle
} from 'lucide-react';
export const OrdersPage: React.FC = () => {
const navigate = useNavigate();
const { orders, reschedulePickup } = useSeller();
const { orders, acceptOrder, trackOrder, getOrderLabel, cancelOrderPickup, reschedulePickup } = useSeller();
const [activeTab, setActiveTab] = useState<'all' | OrderItem['status']>('all');
const [activeOrder, setActiveOrder] = useState<OrderItem | null>(null);
const [showManifest, setShowManifest] = useState(false);
@ -30,26 +31,66 @@ export const OrdersPage: React.FC = () => {
const [showReportDamage, setShowReportDamage] = useState(false);
// Modals state
const [selectedOrderForLabel, setSelectedOrderForLabel] = useState<OrderItem | null>(null);
const [rescheduleOrder, setRescheduleOrder] = useState<OrderItem | null>(null);
const [newSlot, setNewSlot] = useState('Tomorrow, 10:00 AM - 01:00 PM');
const [rescheduleReason, setRescheduleReason] = useState('Packaging / Quality Check in Progress');
const [selectedOrderForLabel, setSelectedOrderForLabel] = useState<any | null>(null);
const [trackingInfo, setTrackingInfo] = useState<any | null>(null);
const [trackingOrderNumber, setTrackingOrderNumber] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const [notice, setNotice] = useState<string | null>(null);
const filteredOrders = activeTab === 'all'
? orders
: orders.filter(o => o.status === activeTab);
const handleConfirmReschedule = (e: React.FormEvent) => {
e.preventDefault();
if (rescheduleOrder) {
reschedulePickup(rescheduleOrder.id, newSlot, rescheduleReason);
setRescheduleOrder(null);
alert(`Pickup rescheduled successfully for order ${rescheduleOrder.orderNumber}`);
const handleAcceptAndShip = async (ord: OrderItem) => {
setIsLoading(true);
setNotice(null);
try {
await acceptOrder(ord.id);
setNotice(`Order ${ord.orderNumber} accepted! Shadowfax delivery pickup created.`);
} catch (e: any) {
alert(`Failed to accept order: ${e.message}`);
} finally {
setIsLoading(false);
}
};
const handlePrintAll = () => {
navigate('/dashboard/orders/labels');
const handleTrackShipment = async (ord: OrderItem) => {
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) {
@ -101,7 +142,7 @@ export const OrdersPage: React.FC = () => {
if (showReportDamage) {
return (
<ReportDamageView
order={orders[0]} // using first order as sample since the mockup shows a specific item
order={orders[0]}
onBack={() => setShowReportDamage(false)}
onTabChange={(tab) => {
if (tab === 'overall') {
@ -127,7 +168,7 @@ export const OrdersPage: React.FC = () => {
<OrderDetailView
order={activeOrder}
onBack={() => setActiveOrder(null)}
onPrintLabel={setSelectedOrderForLabel}
onPrintLabel={() => handleFetchPrintLabel(activeOrder)}
onReschedule={() => {
setActiveOrder(null);
setShowReschedule(true);
@ -165,7 +206,7 @@ export const OrdersPage: React.FC = () => {
</div>
<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">
Manage your incoming customer orders and track pickups.
Manage incoming customer orders, dispatch delivery pickups, and print 4x6 shipping labels.
</p>
</div>
<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" />
Reschedule Pickup
</button>
<button onClick={handlePrintAll} className="button is-rounded is-trad-primary shadow-sm" style={{ height: '40px', fontWeight: 600, fontSize: '0.85rem' }}>
<Printer size={16} className="mr-2" />
Print Today's Labels ({readyForPickupCount})
<button onClick={() => navigate('/dashboard/orders/returns')} className="button is-rounded is-trad-primary shadow-sm" style={{ height: '40px', fontWeight: 600, fontSize: '0.85rem' }}>
<RefreshCw size={16} className="mr-2" />
Returns Tracker
</button>
</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 */}
<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
@ -188,7 +235,7 @@ export const OrdersPage: React.FC = () => {
style={{ fontWeight: 600, ...(activeTab !== 'all' ? { backgroundColor: '#F6F3F2', color: '#564243' } : {}) }}
>
<Package size={16} className="mr-2" />
Overall
Overall ({orders.length})
</button>
<button
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' } : {}) }}
>
<Inbox size={16} className="mr-2" />
Ready for pickup
Ready for pickup ({readyForPickupCount})
</button>
<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" />
@ -304,17 +351,9 @@ export const OrdersPage: React.FC = () => {
</div>
<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' }}>
<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>
</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>
@ -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' }}>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' }}>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 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>
</thead>
<tbody>
{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="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>
{filteredOrders.length === 0 ? (
<tr>
<td colSpan={7} className="has-text-centered py-6 has-text-grey">No orders found in this section.</td>
</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>
</table>
</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>
{/* MODAL 1: 4x6 Thermal Shipping Label View (Faithful to Stitch) */}
{/* MODAL: Thermal Shipping Label View */}
{selectedOrderForLabel && (
<div className="modal is-active">
<div className="modal-background" onClick={() => setSelectedOrderForLabel(null)}></div>
<div className="modal-card" style={{ maxWidth: '440px' }}>
<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">
Thermal 4x6 Shipping Label View
Print 4x6 Delivery Shipping Label
</p>
<button className="delete" aria-label="close" onClick={() => setSelectedOrderForLabel(null)}></button>
</header>
<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="is-flex is-justify-content-space-between is-align-items-center mb-2 pb-2" style={{ borderBottom: '2px solid #000' }}>
<div>
<h4 className="has-text-weight-black is-size-5 mb-0" style={{ letterSpacing: '1px' }}>TRADHOX EXPRESS</h4>
<span className="is-size-7 font-weight-bold">PREPAID AIR LOGISTICS</span>
</div>
<div className="has-text-right">
<span className="has-text-weight-black is-size-5">DEL</span>
<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">EXPRESS DELIVERY</span>
</div>
</div>
<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>
{/* Barcode Simulation */}
<div className="is-flex is-justify-content-center py-2" style={{ background: '#000', height: '38px', letterSpacing: '4px', color: '#fff', fontSize: '10px', alignItems: 'center' }}>
||||||| | ||||| |||| || |||||| | ||||| ||||
</div>
<div className="is-size-7 font-mono mt-1">ORDER ID: {selectedOrderForLabel.orderNumber}</div>
<div className="is-size-7 font-mono mb-1">AWB: {selectedOrderForLabel.awb_number}</div>
{selectedOrderForLabel.barcode_url ? (
<img src={selectedOrderForLabel.barcode_url} alt="QR Code" style={{ width: '120px' }} className="mx-auto" />
) : (
<div className="is-flex is-justify-content-center py-2" style={{ background: '#000', height: '38px', letterSpacing: '4px', color: '#fff', fontSize: '10px', alignItems: 'center' }}>
||||||| | ||||| |||| || |||||| | ||||| ||||
</div>
)}
<div className="is-size-7 font-mono mt-1">ORDER ID: {selectedOrderForLabel.order_id}</div>
</div>
<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-black is-size-6">{selectedOrderForLabel.customerName}</div>
<div>Flat 402, Heritage Residency, MG Road</div>
<div>{selectedOrderForLabel.customerCity}, {selectedOrderForLabel.customerState} - 400001</div>
<div>Phone: +91 98200 XXXXX</div>
<div className="has-text-weight-black is-size-6">{selectedOrderForLabel.recipient?.name}</div>
<div>{selectedOrderForLabel.recipient?.address}</div>
<div>Phone: {selectedOrderForLabel.recipient?.phone}</div>
</div>
<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>Raghavendra Artisan Guild</div>
<div>42, Shilpkala Marg, Sanganer Craft Cluster</div>
<div>Jaipur, Rajasthan - 302029</div>
<div className="has-text-weight-bold mb-1">DISPATCHED FROM (SELLER):</div>
<div>{selectedOrderForLabel.sender?.name}</div>
<div>{selectedOrderForLabel.sender?.address}</div>
<div>Phone: {selectedOrderForLabel.sender?.phone}</div>
</div>
<div className="is-flex is-justify-content-space-between is-align-items-center" style={{ fontSize: '11px' }}>
<div>
<span>Dead Wt: 1.20 Kg</span> | <span>Qty: {selectedOrderForLabel.quantity}</span>
</div>
<div className="has-text-weight-bold">
GI TAG CRAFT
</div>
<div>Item: {selectedOrderForLabel.package_details?.item}</div>
<div className="has-text-weight-bold">Collectible COD: ₹{selectedOrderForLabel.package_details?.collectible_amount}</div>
</div>
</div>
</section>
<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-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" />
<span>Print to Thermal Printer</span>
<span>Print Thermal Label</span>
</button>
</footer>
</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>
);
};

View file

@ -1,18 +1,104 @@
import React from 'react';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSeller } from '../../context/SellerContext';
import {
Search,
Printer,
ChevronLeft,
ChevronRight,
ShieldAlert,
Send
Send,
CheckCircle2,
XCircle,
QrCode,
FileText,
AlertCircle
} from 'lucide-react';
export const ReturnsTrackerPage: React.FC = () => {
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 (
<div className="returns-tracker-page" style={{ maxWidth: '1400px', margin: '0 auto' }}>
@ -26,39 +112,34 @@ export const ReturnsTrackerPage: React.FC = () => {
</nav>
<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' }}>
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>
</div>
<div className="is-flex is-align-items-center gap-3">
<button
onClick={() => navigate('/dashboard/orders')} // Ideally goes to report damage view
onClick={() => navigate('/dashboard/orders')}
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' }}
>
<ShieldAlert size={18} className="mr-2" />
File Damage Claim
File Transit Damage Claim
</button>
</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 */}
<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 */}
<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' }}>
<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>
</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>
<span className="tag is-rounded" style={{ backgroundColor: '#f0eded', color: '#564243', fontWeight: 600 }}>{returnsList.length} Active</span>
</div>
</div>
@ -67,82 +148,219 @@ export const ReturnsTrackerPage: React.FC = () => {
<table className="table is-fullwidth is-hoverable" style={{ borderCollapse: 'collapse' }}>
<thead>
<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' }}>Order & Craft Item</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<br/>Reason</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' }}>Item & Customer</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' }}>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 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" 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' }}>Actions</th>
</tr>
</thead>
<tbody>
{reverseShipments.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.id}</div>
<div className="is-size-7 mt-1" style={{ color: '#564243' }}>{rtn.date}</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>
{returnsList.length === 0 ? (
<tr>
<td colSpan={6} className="has-text-centered py-6 has-text-grey">
<AlertCircle size={32} className="mx-auto mb-2" />
<div>No active return requests found.</div>
</td>
</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>
</table>
</div>
</div>
{/* Pagination Footer */}
<div className="p-4 is-flex is-align-items-center is-justify-content-space-between" style={{ backgroundColor: '#fcf9f8', borderTop: '1px solid #e5e2e1' }}>
<span className="is-size-7" style={{ color: '#564243' }}>
Showing {reverseShipments.length} of {reverseShipments.length} active reverse shipments
</span>
<div className="is-flex is-align-items-center gap-1">
<button className="button is-small is-rounded is-white" style={{ border: 'none', color: '#564243', fontWeight: 500, backgroundColor: 'transparent' }}>Previous</button>
<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>
<button className="button is-small is-rounded is-white" style={{ width: '32px', height: '32px', padding: 0, border: 'none', color: '#564243', backgroundColor: 'transparent' }}>2</button>
<button className="button is-small is-rounded is-white" style={{ border: 'none', color: '#564243', fontWeight: 500, backgroundColor: 'transparent' }}>Next</button>
{/* QC Modal */}
{qcModalReturn && (
<div className="modal is-active">
<div className="modal-background" onClick={() => setQcModalReturn(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">
Configure Doorstep Quality Check (QC)
</p>
<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>
)}
{/* 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>
);
};

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 headers: Record<string, string> = {};
if (userStr) {
const auth = JSON.parse(userStr);
return {
'Authorization': `Bearer ${auth.access_token}`,
'Content-Type': 'application/json'
};
try {
const auth = JSON.parse(userStr);
if (auth.access_token) {
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 = {
@ -99,6 +106,7 @@ export const api = {
return res.json();
},
// Profile & Settings
getProfile: async () => {
const res = await fetch(`${BASE_URL}/profile/`, {
method: 'GET',
@ -108,7 +116,6 @@ export const api = {
return res.json();
},
// Profile
updateProfile: async (profileData: any) => {
const res = await fetch(`${BASE_URL}/profile/`, {
method: 'PUT',
@ -119,6 +126,16 @@ export const api = {
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
getProducts: async () => {
const res = await fetch(`${BASE_URL}/products/`, { headers: getAuthHeaders() });
@ -136,6 +153,16 @@ export const api = {
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) => {
const res = await fetch(`${BASE_URL}/products/${id}/`, {
method: 'DELETE',
@ -145,7 +172,21 @@ export const api = {
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 () => {
const res = await fetch(`${BASE_URL}/orders/`, { headers: getAuthHeaders() });
if (!res.ok) throw new Error('Failed to fetch orders');
@ -161,10 +202,143 @@ export const api = {
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
getWallet: async () => {
const res = await fetch(`${BASE_URL}/wallet/`, { headers: getAuthHeaders() });
if (!res.ok) throw new Error('Failed to fetch wallet');
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();
}
};