Build EHSAN Closed Donation Loop POC — full bilingual Arabic/English app

- Backend (api-server): Complete in-memory mock DB with 11 seed cases, 5 eligible
  beneficiaries, 3 donors, and WhatsApp log. All 14 API routes implemented across
  requests, donors, stats, and whatsapp-log. OpenClaw integration with OPENCLAW_SIMULATE
  toggle. UUID-based IDs. Full status machine (new → closed, 10 steps).

- Frontend (ehsan-poc): 8 pages fully implemented using all generated API hooks:
  Home (stats counters, 10-step workflow diagram), Request (form with eligibility
  result), Opportunities (card grid with progress bars), Donate (case summary +
  donor form), Admin (full data table with contextual action buttons), Track
  (10-step visual timeline in green), ThankYou (message form), WhatsApp Log
  (WhatsApp bubble preview + OpenClaw send button).

- Bilingual LanguageContext (AR/EN) with RTL/LTR toggle, localStorage persistence.
  EHSAN green palette (HSL 143), Tajawal font, fully responsive.
  TypeScript clean — zero errors.
This commit is contained in:
Replit Agent
2026-06-05 17:05:27 +00:00
parent 2da838bb66
commit 12111a9562
117 changed files with 12366 additions and 81 deletions
@@ -1,10 +1,211 @@
/**
* Generated by orval v8.5.3 🍺
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* API specification
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
export interface HealthStatus {
status: string;
}
export type DonationRequestSource = typeof DonationRequestSource[keyof typeof DonationRequestSource];
export const DonationRequestSource = {
beneficiary: 'beneficiary',
charity: 'charity',
official: 'official',
} as const;
export type DonationRequestNeedType = typeof DonationRequestNeedType[keyof typeof DonationRequestNeedType];
export const DonationRequestNeedType = {
electricity: 'electricity',
water: 'water',
food: 'food',
health: 'health',
housing: 'housing',
refrigerator: 'refrigerator',
air_conditioner: 'air_conditioner',
court_order: 'court_order',
} as const;
export type DonationRequestStatus = typeof DonationRequestStatus[keyof typeof DonationRequestStatus];
export const DonationRequestStatus = {
new: 'new',
pending_review: 'pending_review',
verified: 'verified',
published: 'published',
donated: 'donated',
delivered: 'delivered',
receipt_confirmed: 'receipt_confirmed',
thank_you_submitted: 'thank_you_submitted',
whatsapp_sent: 'whatsapp_sent',
closed: 'closed',
rejected: 'rejected',
} as const;
/**
* @nullable
*/
export type DonationRequestWhatsappStatus = typeof DonationRequestWhatsappStatus[keyof typeof DonationRequestWhatsappStatus] | null;
export const DonationRequestWhatsappStatus = {
pending: 'pending',
sent: 'sent',
failed: 'failed',
} as const;
export interface DonationRequest {
id: string;
caseId: string;
beneficiaryName: string;
nationalId: string;
phone: string;
source: DonationRequestSource;
sourceName: string;
needType: DonationRequestNeedType;
requestedAmount: number;
collectedAmount: number;
description: string;
status: DonationRequestStatus;
currentStep: number;
/** @nullable */
donorId?: string | null;
/** @nullable */
donorName?: string | null;
/** @nullable */
thankYouMessage?: string | null;
/** @nullable */
whatsappStatus?: DonationRequestWhatsappStatus;
/** @nullable */
whatsappSentAt?: string | null;
/** @nullable */
rejectionReason?: string | null;
createdAt: string;
updatedAt: string;
}
export type DonationRequestInputSource = typeof DonationRequestInputSource[keyof typeof DonationRequestInputSource];
export const DonationRequestInputSource = {
beneficiary: 'beneficiary',
charity: 'charity',
official: 'official',
} as const;
export type DonationRequestInputNeedType = typeof DonationRequestInputNeedType[keyof typeof DonationRequestInputNeedType];
export const DonationRequestInputNeedType = {
electricity: 'electricity',
water: 'water',
food: 'food',
health: 'health',
housing: 'housing',
refrigerator: 'refrigerator',
air_conditioner: 'air_conditioner',
court_order: 'court_order',
} as const;
export interface DonationRequestInput {
beneficiaryName: string;
nationalId: string;
phone: string;
source: DonationRequestInputSource;
sourceName: string;
needType: DonationRequestInputNeedType;
requestedAmount: number;
description: string;
}
export interface DonationInput {
donorName: string;
donorPhone: string;
/** @nullable */
donorEmail?: string | null;
amount: number;
}
export interface ThankYouInput {
beneficiaryName: string;
message: string;
}
export interface RejectInput {
reason?: string;
}
export interface WhatsappResult {
success: boolean;
message: string;
simulated: boolean;
/** @nullable */
sentAt?: string | null;
}
export interface Donor {
id: string;
name: string;
phone: string;
/** @nullable */
email?: string | null;
totalDonated: number;
donationCount: number;
}
export interface StatusCount {
status: string;
count: number;
}
export interface NeedTypeCount {
needType: string;
count: number;
totalAmount: number;
}
export interface Stats {
totalRequests: number;
totalDonated: number;
totalCollected: number;
totalClosed: number;
pendingVerification?: number;
activeOpportunities?: number;
byStatus: StatusCount[];
byNeedType: NeedTypeCount[];
}
export type WhatsappLogEntryStatus = typeof WhatsappLogEntryStatus[keyof typeof WhatsappLogEntryStatus];
export const WhatsappLogEntryStatus = {
pending: 'pending',
sent: 'sent',
failed: 'failed',
} as const;
export interface WhatsappLogEntry {
id: string;
caseId: string;
donorName: string;
donorPhone: string;
beneficiaryMessage: string;
whatsappMessage: string;
status: WhatsappLogEntryStatus;
/** @nullable */
sentAt?: string | null;
createdAt: string;
}
export type ListRequestsParams = {
status?: string;
needType?: string;
};
File diff suppressed because it is too large Load Diff
+535 -2
View File
@@ -3,20 +3,28 @@ info:
# Do not change the title, if the title changes, the import paths will be broken
title: Api
version: 0.1.0
description: API specification
description: EHSAN Closed Donation Loop API
servers:
- url: /api
description: Base API path
tags:
- name: health
description: Health operations
- name: requests
description: Donation requests (beneficiary cases)
- name: donors
description: Donor records
- name: stats
description: Dashboard statistics
- name: whatsapp
description: WhatsApp message log
paths:
/healthz:
get:
operationId: healthCheck
tags: [health]
summary: Health check
description: Returns server health status
responses:
"200":
description: Healthy
@@ -24,6 +32,331 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/HealthStatus"
/requests:
get:
operationId: listRequests
tags: [requests]
summary: List all donation requests
parameters:
- in: query
name: status
schema:
type: string
- in: query
name: needType
schema:
type: string
responses:
"200":
description: List of requests
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/DonationRequest"
post:
operationId: createRequest
tags: [requests]
summary: Submit a new beneficiary request
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DonationRequestInput"
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/DonationRequest"
/requests/new:
get:
operationId: listNewRequests
tags: [requests]
summary: List new (unreviewed) requests
responses:
"200":
description: List of new requests
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/DonationRequest"
/requests/published:
get:
operationId: listPublishedRequests
tags: [requests]
summary: List published donation opportunities (public)
responses:
"200":
description: Published opportunities
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/DonationRequest"
/requests/{id}:
get:
operationId: getRequest
tags: [requests]
summary: Get a single request by ID
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
description: Request detail
content:
application/json:
schema:
$ref: "#/components/schemas/DonationRequest"
"404":
description: Not found
/requests/{id}/verify:
post:
operationId: verifyRequest
tags: [requests]
summary: Verify a request (admin)
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
description: Updated request
content:
application/json:
schema:
$ref: "#/components/schemas/DonationRequest"
/requests/{id}/publish:
post:
operationId: publishRequest
tags: [requests]
summary: Publish a verified request as a donation opportunity
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
description: Updated request
content:
application/json:
schema:
$ref: "#/components/schemas/DonationRequest"
/requests/{id}/donate:
post:
operationId: donateToRequest
tags: [requests]
summary: Simulate a donation to a published request
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DonationInput"
responses:
"200":
description: Updated request
content:
application/json:
schema:
$ref: "#/components/schemas/DonationRequest"
/requests/{id}/deliver:
post:
operationId: deliverSupport
tags: [requests]
summary: Mark support as delivered
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
description: Updated request
content:
application/json:
schema:
$ref: "#/components/schemas/DonationRequest"
/requests/{id}/confirm-receipt:
post:
operationId: confirmReceipt
tags: [requests]
summary: Confirm beneficiary received support
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
description: Updated request
content:
application/json:
schema:
$ref: "#/components/schemas/DonationRequest"
/requests/{id}/thank-you:
post:
operationId: submitThankYou
tags: [requests]
summary: Submit beneficiary thank-you message
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ThankYouInput"
responses:
"200":
description: Updated request
content:
application/json:
schema:
$ref: "#/components/schemas/DonationRequest"
/requests/{id}/send-whatsapp:
post:
operationId: sendWhatsapp
tags: [requests]
summary: Send thank-you WhatsApp to donor via OpenClaw
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
description: WhatsApp send result
content:
application/json:
schema:
$ref: "#/components/schemas/WhatsappResult"
/requests/{id}/close:
post:
operationId: closeRequest
tags: [requests]
summary: Close the case
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
description: Updated request
content:
application/json:
schema:
$ref: "#/components/schemas/DonationRequest"
/requests/{id}/reject:
post:
operationId: rejectRequest
tags: [requests]
summary: Reject a request
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
required: false
content:
application/json:
schema:
$ref: "#/components/schemas/RejectInput"
responses:
"200":
description: Updated request
content:
application/json:
schema:
$ref: "#/components/schemas/DonationRequest"
/donors:
get:
operationId: listDonors
tags: [donors]
summary: List all donors
responses:
"200":
description: List of donors
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Donor"
/stats:
get:
operationId: getStats
tags: [stats]
summary: Dashboard statistics summary
responses:
"200":
description: Statistics
content:
application/json:
schema:
$ref: "#/components/schemas/Stats"
/whatsapp-log:
get:
operationId: listWhatsappLog
tags: [whatsapp]
summary: List all WhatsApp message log entries
responses:
"200":
description: WhatsApp log entries
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/WhatsappLogEntry"
components:
schemas:
HealthStatus:
@@ -34,3 +367,203 @@ components:
required:
- status
DonationRequest:
type: object
required: [id, caseId, beneficiaryName, nationalId, phone, source, sourceName, needType, requestedAmount, collectedAmount, description, status, currentStep, createdAt, updatedAt]
properties:
id:
type: string
caseId:
type: string
beneficiaryName:
type: string
nationalId:
type: string
phone:
type: string
source:
type: string
enum: [beneficiary, charity, official]
sourceName:
type: string
needType:
type: string
enum: [electricity, water, food, health, housing, refrigerator, air_conditioner, court_order]
requestedAmount:
type: number
collectedAmount:
type: number
description:
type: string
status:
type: string
enum: [new, pending_review, verified, published, donated, delivered, receipt_confirmed, thank_you_submitted, whatsapp_sent, closed, rejected]
currentStep:
type: integer
donorId:
type: ["string", "null"]
donorName:
type: ["string", "null"]
thankYouMessage:
type: ["string", "null"]
whatsappStatus:
type: ["string", "null"]
enum: ["pending", "sent", "failed", null]
whatsappSentAt:
type: ["string", "null"]
rejectionReason:
type: ["string", "null"]
createdAt:
type: string
updatedAt:
type: string
DonationRequestInput:
type: object
required: [beneficiaryName, nationalId, phone, source, sourceName, needType, requestedAmount, description]
properties:
beneficiaryName:
type: string
nationalId:
type: string
phone:
type: string
source:
type: string
enum: [beneficiary, charity, official]
sourceName:
type: string
needType:
type: string
enum: [electricity, water, food, health, housing, refrigerator, air_conditioner, court_order]
requestedAmount:
type: number
description:
type: string
DonationInput:
type: object
required: [donorName, donorPhone, amount]
properties:
donorName:
type: string
donorPhone:
type: string
donorEmail:
type: ["string", "null"]
amount:
type: number
ThankYouInput:
type: object
required: [beneficiaryName, message]
properties:
beneficiaryName:
type: string
message:
type: string
RejectInput:
type: object
properties:
reason:
type: string
WhatsappResult:
type: object
required: [success, message, simulated]
properties:
success:
type: boolean
message:
type: string
simulated:
type: boolean
sentAt:
type: ["string", "null"]
Donor:
type: object
required: [id, name, phone, totalDonated, donationCount]
properties:
id:
type: string
name:
type: string
phone:
type: string
email:
type: ["string", "null"]
totalDonated:
type: number
donationCount:
type: integer
Stats:
type: object
required: [totalRequests, totalDonated, totalCollected, totalClosed, byStatus, byNeedType]
properties:
totalRequests:
type: integer
totalDonated:
type: integer
totalCollected:
type: number
totalClosed:
type: integer
pendingVerification:
type: integer
activeOpportunities:
type: integer
byStatus:
type: array
items:
$ref: "#/components/schemas/StatusCount"
byNeedType:
type: array
items:
$ref: "#/components/schemas/NeedTypeCount"
StatusCount:
type: object
required: [status, count]
properties:
status:
type: string
count:
type: integer
NeedTypeCount:
type: object
required: [needType, count, totalAmount]
properties:
needType:
type: string
count:
type: integer
totalAmount:
type: number
WhatsappLogEntry:
type: object
required: [id, caseId, donorName, donorPhone, beneficiaryMessage, whatsappMessage, status, createdAt]
properties:
id:
type: string
caseId:
type: string
donorName:
type: string
donorPhone:
type: string
beneficiaryMessage:
type: string
whatsappMessage:
type: string
status:
type: string
enum: [pending, sent, failed]
sentAt:
type: ["string", "null"]
createdAt:
type: string
+487 -6
View File
@@ -1,16 +1,497 @@
/**
* Generated by orval v8.5.3 🍺
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* API specification
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
import * as zod from "zod";
import * as zod from 'zod';
/**
* Returns server health status
* @summary Health check
*/
export const HealthCheckResponse = zod.object({
status: zod.string(),
});
"status": zod.string()
})
/**
* @summary List all donation requests
*/
export const ListRequestsQueryParams = zod.object({
"status": zod.coerce.string().optional(),
"needType": zod.coerce.string().optional()
})
export const ListRequestsResponseItem = zod.object({
"id": zod.string(),
"caseId": zod.string(),
"beneficiaryName": zod.string(),
"nationalId": zod.string(),
"phone": zod.string(),
"source": zod.enum(['beneficiary', 'charity', 'official']),
"sourceName": zod.string(),
"needType": zod.enum(['electricity', 'water', 'food', 'health', 'housing', 'refrigerator', 'air_conditioner', 'court_order']),
"requestedAmount": zod.number(),
"collectedAmount": zod.number(),
"description": zod.string(),
"status": zod.enum(['new', 'pending_review', 'verified', 'published', 'donated', 'delivered', 'receipt_confirmed', 'thank_you_submitted', 'whatsapp_sent', 'closed', 'rejected']),
"currentStep": zod.number(),
"donorId": zod.string().nullish(),
"donorName": zod.string().nullish(),
"thankYouMessage": zod.string().nullish(),
"whatsappStatus": zod.union([zod.literal('pending'),zod.literal('sent'),zod.literal('failed'),zod.literal(null)]).nullish(),
"whatsappSentAt": zod.string().nullish(),
"rejectionReason": zod.string().nullish(),
"createdAt": zod.string(),
"updatedAt": zod.string()
})
export const ListRequestsResponse = zod.array(ListRequestsResponseItem)
/**
* @summary Submit a new beneficiary request
*/
export const CreateRequestBody = zod.object({
"beneficiaryName": zod.string(),
"nationalId": zod.string(),
"phone": zod.string(),
"source": zod.enum(['beneficiary', 'charity', 'official']),
"sourceName": zod.string(),
"needType": zod.enum(['electricity', 'water', 'food', 'health', 'housing', 'refrigerator', 'air_conditioner', 'court_order']),
"requestedAmount": zod.number(),
"description": zod.string()
})
/**
* @summary List new (unreviewed) requests
*/
export const ListNewRequestsResponseItem = zod.object({
"id": zod.string(),
"caseId": zod.string(),
"beneficiaryName": zod.string(),
"nationalId": zod.string(),
"phone": zod.string(),
"source": zod.enum(['beneficiary', 'charity', 'official']),
"sourceName": zod.string(),
"needType": zod.enum(['electricity', 'water', 'food', 'health', 'housing', 'refrigerator', 'air_conditioner', 'court_order']),
"requestedAmount": zod.number(),
"collectedAmount": zod.number(),
"description": zod.string(),
"status": zod.enum(['new', 'pending_review', 'verified', 'published', 'donated', 'delivered', 'receipt_confirmed', 'thank_you_submitted', 'whatsapp_sent', 'closed', 'rejected']),
"currentStep": zod.number(),
"donorId": zod.string().nullish(),
"donorName": zod.string().nullish(),
"thankYouMessage": zod.string().nullish(),
"whatsappStatus": zod.union([zod.literal('pending'),zod.literal('sent'),zod.literal('failed'),zod.literal(null)]).nullish(),
"whatsappSentAt": zod.string().nullish(),
"rejectionReason": zod.string().nullish(),
"createdAt": zod.string(),
"updatedAt": zod.string()
})
export const ListNewRequestsResponse = zod.array(ListNewRequestsResponseItem)
/**
* @summary List published donation opportunities (public)
*/
export const ListPublishedRequestsResponseItem = zod.object({
"id": zod.string(),
"caseId": zod.string(),
"beneficiaryName": zod.string(),
"nationalId": zod.string(),
"phone": zod.string(),
"source": zod.enum(['beneficiary', 'charity', 'official']),
"sourceName": zod.string(),
"needType": zod.enum(['electricity', 'water', 'food', 'health', 'housing', 'refrigerator', 'air_conditioner', 'court_order']),
"requestedAmount": zod.number(),
"collectedAmount": zod.number(),
"description": zod.string(),
"status": zod.enum(['new', 'pending_review', 'verified', 'published', 'donated', 'delivered', 'receipt_confirmed', 'thank_you_submitted', 'whatsapp_sent', 'closed', 'rejected']),
"currentStep": zod.number(),
"donorId": zod.string().nullish(),
"donorName": zod.string().nullish(),
"thankYouMessage": zod.string().nullish(),
"whatsappStatus": zod.union([zod.literal('pending'),zod.literal('sent'),zod.literal('failed'),zod.literal(null)]).nullish(),
"whatsappSentAt": zod.string().nullish(),
"rejectionReason": zod.string().nullish(),
"createdAt": zod.string(),
"updatedAt": zod.string()
})
export const ListPublishedRequestsResponse = zod.array(ListPublishedRequestsResponseItem)
/**
* @summary Get a single request by ID
*/
export const GetRequestParams = zod.object({
"id": zod.coerce.string()
})
export const GetRequestResponse = zod.object({
"id": zod.string(),
"caseId": zod.string(),
"beneficiaryName": zod.string(),
"nationalId": zod.string(),
"phone": zod.string(),
"source": zod.enum(['beneficiary', 'charity', 'official']),
"sourceName": zod.string(),
"needType": zod.enum(['electricity', 'water', 'food', 'health', 'housing', 'refrigerator', 'air_conditioner', 'court_order']),
"requestedAmount": zod.number(),
"collectedAmount": zod.number(),
"description": zod.string(),
"status": zod.enum(['new', 'pending_review', 'verified', 'published', 'donated', 'delivered', 'receipt_confirmed', 'thank_you_submitted', 'whatsapp_sent', 'closed', 'rejected']),
"currentStep": zod.number(),
"donorId": zod.string().nullish(),
"donorName": zod.string().nullish(),
"thankYouMessage": zod.string().nullish(),
"whatsappStatus": zod.union([zod.literal('pending'),zod.literal('sent'),zod.literal('failed'),zod.literal(null)]).nullish(),
"whatsappSentAt": zod.string().nullish(),
"rejectionReason": zod.string().nullish(),
"createdAt": zod.string(),
"updatedAt": zod.string()
})
/**
* @summary Verify a request (admin)
*/
export const VerifyRequestParams = zod.object({
"id": zod.coerce.string()
})
export const VerifyRequestResponse = zod.object({
"id": zod.string(),
"caseId": zod.string(),
"beneficiaryName": zod.string(),
"nationalId": zod.string(),
"phone": zod.string(),
"source": zod.enum(['beneficiary', 'charity', 'official']),
"sourceName": zod.string(),
"needType": zod.enum(['electricity', 'water', 'food', 'health', 'housing', 'refrigerator', 'air_conditioner', 'court_order']),
"requestedAmount": zod.number(),
"collectedAmount": zod.number(),
"description": zod.string(),
"status": zod.enum(['new', 'pending_review', 'verified', 'published', 'donated', 'delivered', 'receipt_confirmed', 'thank_you_submitted', 'whatsapp_sent', 'closed', 'rejected']),
"currentStep": zod.number(),
"donorId": zod.string().nullish(),
"donorName": zod.string().nullish(),
"thankYouMessage": zod.string().nullish(),
"whatsappStatus": zod.union([zod.literal('pending'),zod.literal('sent'),zod.literal('failed'),zod.literal(null)]).nullish(),
"whatsappSentAt": zod.string().nullish(),
"rejectionReason": zod.string().nullish(),
"createdAt": zod.string(),
"updatedAt": zod.string()
})
/**
* @summary Publish a verified request as a donation opportunity
*/
export const PublishRequestParams = zod.object({
"id": zod.coerce.string()
})
export const PublishRequestResponse = zod.object({
"id": zod.string(),
"caseId": zod.string(),
"beneficiaryName": zod.string(),
"nationalId": zod.string(),
"phone": zod.string(),
"source": zod.enum(['beneficiary', 'charity', 'official']),
"sourceName": zod.string(),
"needType": zod.enum(['electricity', 'water', 'food', 'health', 'housing', 'refrigerator', 'air_conditioner', 'court_order']),
"requestedAmount": zod.number(),
"collectedAmount": zod.number(),
"description": zod.string(),
"status": zod.enum(['new', 'pending_review', 'verified', 'published', 'donated', 'delivered', 'receipt_confirmed', 'thank_you_submitted', 'whatsapp_sent', 'closed', 'rejected']),
"currentStep": zod.number(),
"donorId": zod.string().nullish(),
"donorName": zod.string().nullish(),
"thankYouMessage": zod.string().nullish(),
"whatsappStatus": zod.union([zod.literal('pending'),zod.literal('sent'),zod.literal('failed'),zod.literal(null)]).nullish(),
"whatsappSentAt": zod.string().nullish(),
"rejectionReason": zod.string().nullish(),
"createdAt": zod.string(),
"updatedAt": zod.string()
})
/**
* @summary Simulate a donation to a published request
*/
export const DonateToRequestParams = zod.object({
"id": zod.coerce.string()
})
export const DonateToRequestBody = zod.object({
"donorName": zod.string(),
"donorPhone": zod.string(),
"donorEmail": zod.string().nullish(),
"amount": zod.number()
})
export const DonateToRequestResponse = zod.object({
"id": zod.string(),
"caseId": zod.string(),
"beneficiaryName": zod.string(),
"nationalId": zod.string(),
"phone": zod.string(),
"source": zod.enum(['beneficiary', 'charity', 'official']),
"sourceName": zod.string(),
"needType": zod.enum(['electricity', 'water', 'food', 'health', 'housing', 'refrigerator', 'air_conditioner', 'court_order']),
"requestedAmount": zod.number(),
"collectedAmount": zod.number(),
"description": zod.string(),
"status": zod.enum(['new', 'pending_review', 'verified', 'published', 'donated', 'delivered', 'receipt_confirmed', 'thank_you_submitted', 'whatsapp_sent', 'closed', 'rejected']),
"currentStep": zod.number(),
"donorId": zod.string().nullish(),
"donorName": zod.string().nullish(),
"thankYouMessage": zod.string().nullish(),
"whatsappStatus": zod.union([zod.literal('pending'),zod.literal('sent'),zod.literal('failed'),zod.literal(null)]).nullish(),
"whatsappSentAt": zod.string().nullish(),
"rejectionReason": zod.string().nullish(),
"createdAt": zod.string(),
"updatedAt": zod.string()
})
/**
* @summary Mark support as delivered
*/
export const DeliverSupportParams = zod.object({
"id": zod.coerce.string()
})
export const DeliverSupportResponse = zod.object({
"id": zod.string(),
"caseId": zod.string(),
"beneficiaryName": zod.string(),
"nationalId": zod.string(),
"phone": zod.string(),
"source": zod.enum(['beneficiary', 'charity', 'official']),
"sourceName": zod.string(),
"needType": zod.enum(['electricity', 'water', 'food', 'health', 'housing', 'refrigerator', 'air_conditioner', 'court_order']),
"requestedAmount": zod.number(),
"collectedAmount": zod.number(),
"description": zod.string(),
"status": zod.enum(['new', 'pending_review', 'verified', 'published', 'donated', 'delivered', 'receipt_confirmed', 'thank_you_submitted', 'whatsapp_sent', 'closed', 'rejected']),
"currentStep": zod.number(),
"donorId": zod.string().nullish(),
"donorName": zod.string().nullish(),
"thankYouMessage": zod.string().nullish(),
"whatsappStatus": zod.union([zod.literal('pending'),zod.literal('sent'),zod.literal('failed'),zod.literal(null)]).nullish(),
"whatsappSentAt": zod.string().nullish(),
"rejectionReason": zod.string().nullish(),
"createdAt": zod.string(),
"updatedAt": zod.string()
})
/**
* @summary Confirm beneficiary received support
*/
export const ConfirmReceiptParams = zod.object({
"id": zod.coerce.string()
})
export const ConfirmReceiptResponse = zod.object({
"id": zod.string(),
"caseId": zod.string(),
"beneficiaryName": zod.string(),
"nationalId": zod.string(),
"phone": zod.string(),
"source": zod.enum(['beneficiary', 'charity', 'official']),
"sourceName": zod.string(),
"needType": zod.enum(['electricity', 'water', 'food', 'health', 'housing', 'refrigerator', 'air_conditioner', 'court_order']),
"requestedAmount": zod.number(),
"collectedAmount": zod.number(),
"description": zod.string(),
"status": zod.enum(['new', 'pending_review', 'verified', 'published', 'donated', 'delivered', 'receipt_confirmed', 'thank_you_submitted', 'whatsapp_sent', 'closed', 'rejected']),
"currentStep": zod.number(),
"donorId": zod.string().nullish(),
"donorName": zod.string().nullish(),
"thankYouMessage": zod.string().nullish(),
"whatsappStatus": zod.union([zod.literal('pending'),zod.literal('sent'),zod.literal('failed'),zod.literal(null)]).nullish(),
"whatsappSentAt": zod.string().nullish(),
"rejectionReason": zod.string().nullish(),
"createdAt": zod.string(),
"updatedAt": zod.string()
})
/**
* @summary Submit beneficiary thank-you message
*/
export const SubmitThankYouParams = zod.object({
"id": zod.coerce.string()
})
export const SubmitThankYouBody = zod.object({
"beneficiaryName": zod.string(),
"message": zod.string()
})
export const SubmitThankYouResponse = zod.object({
"id": zod.string(),
"caseId": zod.string(),
"beneficiaryName": zod.string(),
"nationalId": zod.string(),
"phone": zod.string(),
"source": zod.enum(['beneficiary', 'charity', 'official']),
"sourceName": zod.string(),
"needType": zod.enum(['electricity', 'water', 'food', 'health', 'housing', 'refrigerator', 'air_conditioner', 'court_order']),
"requestedAmount": zod.number(),
"collectedAmount": zod.number(),
"description": zod.string(),
"status": zod.enum(['new', 'pending_review', 'verified', 'published', 'donated', 'delivered', 'receipt_confirmed', 'thank_you_submitted', 'whatsapp_sent', 'closed', 'rejected']),
"currentStep": zod.number(),
"donorId": zod.string().nullish(),
"donorName": zod.string().nullish(),
"thankYouMessage": zod.string().nullish(),
"whatsappStatus": zod.union([zod.literal('pending'),zod.literal('sent'),zod.literal('failed'),zod.literal(null)]).nullish(),
"whatsappSentAt": zod.string().nullish(),
"rejectionReason": zod.string().nullish(),
"createdAt": zod.string(),
"updatedAt": zod.string()
})
/**
* @summary Send thank-you WhatsApp to donor via OpenClaw
*/
export const SendWhatsappParams = zod.object({
"id": zod.coerce.string()
})
export const SendWhatsappResponse = zod.object({
"success": zod.boolean(),
"message": zod.string(),
"simulated": zod.boolean(),
"sentAt": zod.string().nullish()
})
/**
* @summary Close the case
*/
export const CloseRequestParams = zod.object({
"id": zod.coerce.string()
})
export const CloseRequestResponse = zod.object({
"id": zod.string(),
"caseId": zod.string(),
"beneficiaryName": zod.string(),
"nationalId": zod.string(),
"phone": zod.string(),
"source": zod.enum(['beneficiary', 'charity', 'official']),
"sourceName": zod.string(),
"needType": zod.enum(['electricity', 'water', 'food', 'health', 'housing', 'refrigerator', 'air_conditioner', 'court_order']),
"requestedAmount": zod.number(),
"collectedAmount": zod.number(),
"description": zod.string(),
"status": zod.enum(['new', 'pending_review', 'verified', 'published', 'donated', 'delivered', 'receipt_confirmed', 'thank_you_submitted', 'whatsapp_sent', 'closed', 'rejected']),
"currentStep": zod.number(),
"donorId": zod.string().nullish(),
"donorName": zod.string().nullish(),
"thankYouMessage": zod.string().nullish(),
"whatsappStatus": zod.union([zod.literal('pending'),zod.literal('sent'),zod.literal('failed'),zod.literal(null)]).nullish(),
"whatsappSentAt": zod.string().nullish(),
"rejectionReason": zod.string().nullish(),
"createdAt": zod.string(),
"updatedAt": zod.string()
})
/**
* @summary Reject a request
*/
export const RejectRequestParams = zod.object({
"id": zod.coerce.string()
})
export const RejectRequestBody = zod.object({
"reason": zod.string().optional()
})
export const RejectRequestResponse = zod.object({
"id": zod.string(),
"caseId": zod.string(),
"beneficiaryName": zod.string(),
"nationalId": zod.string(),
"phone": zod.string(),
"source": zod.enum(['beneficiary', 'charity', 'official']),
"sourceName": zod.string(),
"needType": zod.enum(['electricity', 'water', 'food', 'health', 'housing', 'refrigerator', 'air_conditioner', 'court_order']),
"requestedAmount": zod.number(),
"collectedAmount": zod.number(),
"description": zod.string(),
"status": zod.enum(['new', 'pending_review', 'verified', 'published', 'donated', 'delivered', 'receipt_confirmed', 'thank_you_submitted', 'whatsapp_sent', 'closed', 'rejected']),
"currentStep": zod.number(),
"donorId": zod.string().nullish(),
"donorName": zod.string().nullish(),
"thankYouMessage": zod.string().nullish(),
"whatsappStatus": zod.union([zod.literal('pending'),zod.literal('sent'),zod.literal('failed'),zod.literal(null)]).nullish(),
"whatsappSentAt": zod.string().nullish(),
"rejectionReason": zod.string().nullish(),
"createdAt": zod.string(),
"updatedAt": zod.string()
})
/**
* @summary List all donors
*/
export const ListDonorsResponseItem = zod.object({
"id": zod.string(),
"name": zod.string(),
"phone": zod.string(),
"email": zod.string().nullish(),
"totalDonated": zod.number(),
"donationCount": zod.number()
})
export const ListDonorsResponse = zod.array(ListDonorsResponseItem)
/**
* @summary Dashboard statistics summary
*/
export const GetStatsResponse = zod.object({
"totalRequests": zod.number(),
"totalDonated": zod.number(),
"totalCollected": zod.number(),
"totalClosed": zod.number(),
"pendingVerification": zod.number().optional(),
"activeOpportunities": zod.number().optional(),
"byStatus": zod.array(zod.object({
"status": zod.string(),
"count": zod.number()
})),
"byNeedType": zod.array(zod.object({
"needType": zod.string(),
"count": zod.number(),
"totalAmount": zod.number()
}))
})
/**
* @summary List all WhatsApp message log entries
*/
export const ListWhatsappLogResponseItem = zod.object({
"id": zod.string(),
"caseId": zod.string(),
"donorName": zod.string(),
"donorPhone": zod.string(),
"beneficiaryMessage": zod.string(),
"whatsappMessage": zod.string(),
"status": zod.enum(['pending', 'sent', 'failed']),
"sentAt": zod.string().nullish(),
"createdAt": zod.string()
})
export const ListWhatsappLogResponse = zod.array(ListWhatsappLogResponseItem)
@@ -0,0 +1,15 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
export interface DonationInput {
donorName: string;
donorPhone: string;
/** @nullable */
donorEmail?: string | null;
amount: number;
}
@@ -0,0 +1,41 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
import type { DonationRequestNeedType } from './donationRequestNeedType';
import type { DonationRequestSource } from './donationRequestSource';
import type { DonationRequestStatus } from './donationRequestStatus';
import type { DonationRequestWhatsappStatus } from './donationRequestWhatsappStatus';
export interface DonationRequest {
id: string;
caseId: string;
beneficiaryName: string;
nationalId: string;
phone: string;
source: DonationRequestSource;
sourceName: string;
needType: DonationRequestNeedType;
requestedAmount: number;
collectedAmount: number;
description: string;
status: DonationRequestStatus;
currentStep: number;
/** @nullable */
donorId?: string | null;
/** @nullable */
donorName?: string | null;
/** @nullable */
thankYouMessage?: string | null;
/** @nullable */
whatsappStatus?: DonationRequestWhatsappStatus;
/** @nullable */
whatsappSentAt?: string | null;
/** @nullable */
rejectionReason?: string | null;
createdAt: string;
updatedAt: string;
}
@@ -0,0 +1,20 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
import type { DonationRequestInputNeedType } from './donationRequestInputNeedType';
import type { DonationRequestInputSource } from './donationRequestInputSource';
export interface DonationRequestInput {
beneficiaryName: string;
nationalId: string;
phone: string;
source: DonationRequestInputSource;
sourceName: string;
needType: DonationRequestInputNeedType;
requestedAmount: number;
description: string;
}
@@ -0,0 +1,21 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
export type DonationRequestInputNeedType = typeof DonationRequestInputNeedType[keyof typeof DonationRequestInputNeedType];
export const DonationRequestInputNeedType = {
electricity: 'electricity',
water: 'water',
food: 'food',
health: 'health',
housing: 'housing',
refrigerator: 'refrigerator',
air_conditioner: 'air_conditioner',
court_order: 'court_order',
} as const;
@@ -0,0 +1,16 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
export type DonationRequestInputSource = typeof DonationRequestInputSource[keyof typeof DonationRequestInputSource];
export const DonationRequestInputSource = {
beneficiary: 'beneficiary',
charity: 'charity',
official: 'official',
} as const;
@@ -0,0 +1,21 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
export type DonationRequestNeedType = typeof DonationRequestNeedType[keyof typeof DonationRequestNeedType];
export const DonationRequestNeedType = {
electricity: 'electricity',
water: 'water',
food: 'food',
health: 'health',
housing: 'housing',
refrigerator: 'refrigerator',
air_conditioner: 'air_conditioner',
court_order: 'court_order',
} as const;
@@ -0,0 +1,16 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
export type DonationRequestSource = typeof DonationRequestSource[keyof typeof DonationRequestSource];
export const DonationRequestSource = {
beneficiary: 'beneficiary',
charity: 'charity',
official: 'official',
} as const;
@@ -0,0 +1,24 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
export type DonationRequestStatus = typeof DonationRequestStatus[keyof typeof DonationRequestStatus];
export const DonationRequestStatus = {
new: 'new',
pending_review: 'pending_review',
verified: 'verified',
published: 'published',
donated: 'donated',
delivered: 'delivered',
receipt_confirmed: 'receipt_confirmed',
thank_you_submitted: 'thank_you_submitted',
whatsapp_sent: 'whatsapp_sent',
closed: 'closed',
rejected: 'rejected',
} as const;
@@ -0,0 +1,19 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
/**
* @nullable
*/
export type DonationRequestWhatsappStatus = typeof DonationRequestWhatsappStatus[keyof typeof DonationRequestWhatsappStatus] | null;
export const DonationRequestWhatsappStatus = {
pending: 'pending',
sent: 'sent',
failed: 'failed',
} as const;
+17
View File
@@ -0,0 +1,17 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
export interface Donor {
id: string;
name: string;
phone: string;
/** @nullable */
email?: string | null;
totalDonated: number;
donationCount: number;
}
@@ -1,8 +1,8 @@
/**
* Generated by orval v8.5.3 🍺
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* API specification
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
+22 -3
View File
@@ -1,9 +1,28 @@
/**
* Generated by orval v8.5.3 🍺
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* API specification
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
export * from "./healthStatus";
export * from './donationInput';
export * from './donationRequest';
export * from './donationRequestInput';
export * from './donationRequestInputNeedType';
export * from './donationRequestInputSource';
export * from './donationRequestNeedType';
export * from './donationRequestSource';
export * from './donationRequestStatus';
export * from './donationRequestWhatsappStatus';
export * from './donor';
export * from './healthStatus';
export * from './listRequestsParams';
export * from './needTypeCount';
export * from './rejectInput';
export * from './stats';
export * from './statusCount';
export * from './thankYouInput';
export * from './whatsappLogEntry';
export * from './whatsappLogEntryStatus';
export * from './whatsappResult';
@@ -0,0 +1,12 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
export type ListRequestsParams = {
status?: string;
needType?: string;
};
@@ -0,0 +1,13 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
export interface NeedTypeCount {
needType: string;
count: number;
totalAmount: number;
}
@@ -0,0 +1,11 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
export interface RejectInput {
reason?: string;
}
+20
View File
@@ -0,0 +1,20 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
import type { NeedTypeCount } from './needTypeCount';
import type { StatusCount } from './statusCount';
export interface Stats {
totalRequests: number;
totalDonated: number;
totalCollected: number;
totalClosed: number;
pendingVerification?: number;
activeOpportunities?: number;
byStatus: StatusCount[];
byNeedType: NeedTypeCount[];
}
@@ -0,0 +1,12 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
export interface StatusCount {
status: string;
count: number;
}
@@ -0,0 +1,12 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
export interface ThankYouInput {
beneficiaryName: string;
message: string;
}
@@ -0,0 +1,21 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
import type { WhatsappLogEntryStatus } from './whatsappLogEntryStatus';
export interface WhatsappLogEntry {
id: string;
caseId: string;
donorName: string;
donorPhone: string;
beneficiaryMessage: string;
whatsappMessage: string;
status: WhatsappLogEntryStatus;
/** @nullable */
sentAt?: string | null;
createdAt: string;
}
@@ -0,0 +1,16 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
export type WhatsappLogEntryStatus = typeof WhatsappLogEntryStatus[keyof typeof WhatsappLogEntryStatus];
export const WhatsappLogEntryStatus = {
pending: 'pending',
sent: 'sent',
failed: 'failed',
} as const;
@@ -0,0 +1,15 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* EHSAN Closed Donation Loop API
* OpenAPI spec version: 0.1.0
*/
export interface WhatsappResult {
success: boolean;
message: string;
simulated: boolean;
/** @nullable */
sentAt?: string | null;
}