payment-items.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. 'use client'
  2. import { useEffect, useMemo, useState } from 'react'
  3. import { useFormContext, useWatch } from 'react-hook-form'
  4. import Field from './_field'
  5. import { View, Row } from 'app/design/view'
  6. import { Text } from 'app/design/typography'
  7. import { Input } from 'app/design/controls'
  8. import CheckBox from 'app/ui/atoms/checkbox'
  9. import Loading from 'app/ui/atoms/loading'
  10. import { useFetch } from 'app/lib/hooks/use-fetch'
  11. import { useTranslation } from 'react-i18next'
  12. function resolveItemsUrl(requestUrl, moduleId) {
  13. if (!requestUrl || !moduleId) return null
  14. if (requestUrl.includes('api.php')) {
  15. return requestUrl.endsWith('=') || requestUrl.endsWith('/')
  16. ? requestUrl + moduleId
  17. : requestUrl + moduleId
  18. }
  19. return '/api.php?r=' + requestUrl + moduleId
  20. }
  21. function normalizeItems(payload) {
  22. if (!payload) return []
  23. if (Array.isArray(payload)) return payload
  24. if (Array.isArray(payload.items)) return payload.items
  25. if (Array.isArray(payload.data)) return payload.data
  26. if (Array.isArray(payload?.data?.items)) return payload.data.items
  27. return []
  28. }
  29. function itemPrice(item) {
  30. const raw =
  31. item?.price ??
  32. item?.price_single ??
  33. item?.price_f ??
  34. item?.[0] ??
  35. 0
  36. const n = Number(raw)
  37. return Number.isFinite(n) ? n : 0
  38. }
  39. function itemTitle(item) {
  40. return String(item?.title || item?.name || item?.id || '')
  41. }
  42. export default function PaymentItems(props) {
  43. const { t } = useTranslation()
  44. const formContext = useFormContext()
  45. const moduleId = useWatch({ control: formContext.control, name: 'module_id' })
  46. const sellerId = useWatch({ control: formContext.control, name: 'seller_id' })
  47. const [filter, setFilter] = useState('')
  48. const [selected, setSelected] = useState({})
  49. const requestUrl =
  50. props.request_url ||
  51. props.module_request_url ||
  52. '/api.php?r=bx_payment/get_items&params[]=single&params[]='
  53. const id = Number(moduleId)
  54. let itemsUrl = id ? resolveItemsUrl(requestUrl, id) : null
  55. if (itemsUrl && sellerId) {
  56. itemsUrl += (itemsUrl.includes('?') ? '&' : '?') + 'seller_id=' + encodeURIComponent(sellerId)
  57. }
  58. // isInitialLoading: no spinner for a disabled query or a cached module.
  59. const { data: itemsResponse, isInitialLoading: loading } = useFetch(itemsUrl)
  60. const items = useMemo(
  61. () => (itemsUrl ? normalizeItems(itemsResponse?.data ?? itemsResponse) : []),
  62. [itemsUrl, itemsResponse]
  63. )
  64. // Module cleared → drop the selection too.
  65. useEffect(() => {
  66. if (id) return
  67. setSelected({})
  68. formContext.setValue('items', [])
  69. // eslint-disable-next-line react-hooks/exhaustive-deps
  70. }, [id])
  71. useEffect(() => {
  72. const ids = Object.keys(selected).filter((id) => selected[id]?.checked)
  73. formContext.setValue('items', ids)
  74. ids.forEach((id) => {
  75. const row = selected[id]
  76. formContext.setValue(`item-price-${id}`, String(row.price ?? ''))
  77. formContext.setValue(`item-quantity-${id}`, String(row.quantity ?? 1))
  78. })
  79. // eslint-disable-next-line react-hooks/exhaustive-deps
  80. }, [selected])
  81. const toggleItem = (item) => {
  82. const id = String(item.id)
  83. setSelected((prev) => {
  84. const next = { ...prev }
  85. if (next[id]?.checked) {
  86. next[id] = { ...next[id], checked: false }
  87. } else {
  88. next[id] = {
  89. checked: true,
  90. price: next[id]?.price ?? itemPrice(item),
  91. quantity: next[id]?.quantity ?? 1,
  92. }
  93. }
  94. return next
  95. })
  96. }
  97. const updateField = (id, field, value, item) => {
  98. setSelected((prev) => ({
  99. ...prev,
  100. [id]: {
  101. checked: true,
  102. price: prev[id]?.price ?? itemPrice(item),
  103. quantity: prev[id]?.quantity ?? 1,
  104. ...prev[id],
  105. [field]: value,
  106. },
  107. }))
  108. }
  109. if (!Number(moduleId)) {
  110. return null
  111. }
  112. const filterLower = filter.trim().toLowerCase()
  113. const visibleItems = filterLower
  114. ? items.filter((item) => itemTitle(item).toLowerCase().includes(filterLower))
  115. : items
  116. return (
  117. <Field {...props} error2={formContext.formState.errors?.items}>
  118. <View className="w-full gap-2 border border-border/60 rounded-xl p-3 bg-card">
  119. <Input
  120. placeholder={t('Search')}
  121. value={filter}
  122. onChangeText={setFilter}
  123. />
  124. <Row className="items-center gap-2">
  125. <Text className="flex-1 min-w-0 text-sm font-medium text-muted-foreground">
  126. {t('Product')}
  127. </Text>
  128. <Text className="w-20 shrink-0 text-sm font-medium text-muted-foreground text-right">
  129. {t('Price')}
  130. </Text>
  131. <Text className="w-24 shrink-0 text-sm font-medium text-muted-foreground text-center">
  132. {t('Sold Price')}
  133. </Text>
  134. <Text className="w-14 shrink-0 text-sm font-medium text-muted-foreground text-center">
  135. {t('Qt.')}
  136. </Text>
  137. </Row>
  138. {loading ? (
  139. <View className="items-center py-4">
  140. <Loading />
  141. </View>
  142. ) : visibleItems.length === 0 ? (
  143. <Text className="text-sm text-muted-foreground text-center py-3">
  144. {t('Nothing found')}
  145. </Text>
  146. ) : (
  147. <View className="gap-2">
  148. {visibleItems.map((item) => {
  149. const id = String(item.id)
  150. const checked = !!selected[id]?.checked
  151. const price = itemPrice(item)
  152. return (
  153. <Row
  154. key={id}
  155. className="items-center gap-2 py-1 border-t border-border/40"
  156. >
  157. <View className="flex-1 min-w-0">
  158. <CheckBox
  159. value={checked}
  160. status={checked ? 'checked' : 'unchecked'}
  161. onPress={() => toggleItem(item)}
  162. title={itemTitle(item)}
  163. />
  164. </View>
  165. <Text className="w-20 shrink-0 text-sm text-foreground text-right">
  166. {price.toFixed(2)}
  167. </Text>
  168. <View className="w-24 shrink-0">
  169. <Input
  170. value={String(
  171. selected[id]?.price ?? price
  172. )}
  173. onChangeText={(text) =>
  174. updateField(id, 'price', text, item)
  175. }
  176. keyboardType="decimal-pad"
  177. />
  178. </View>
  179. <View className="w-14 shrink-0">
  180. <Input
  181. value={String(
  182. selected[id]?.quantity ?? 1
  183. )}
  184. onChangeText={(text) =>
  185. updateField(id, 'quantity', text, item)
  186. }
  187. keyboardType="number-pad"
  188. />
  189. </View>
  190. </Row>
  191. )
  192. })}
  193. </View>
  194. )}
  195. </View>
  196. </Field>
  197. )
  198. }