course-structure.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import { View, Row } from 'app/design/view'
  2. import { Text } from 'app/design/typography'
  3. import DropdownMenu from 'app/ui/atoms/dropdown-menu';
  4. import { NeoButton } from 'app/design/controls'
  5. import Link from 'app/ui/atoms/link'
  6. import Card from 'app/ui/molecules/page/card'
  7. import Progress from 'app/ui/atoms/progress'
  8. import Scroll from 'app/ui/molecules/page/scroll'
  9. import { memo } from 'react'
  10. import { useReducer } from 'react'
  11. import { fetcher } from 'app/lib/fetcher'
  12. import { BlockByDataInt as BlockByData } from 'app/components/block';
  13. import { useBottomSheetData } from 'app/context/bottomsheet';
  14. import { DragContext, DragItem, DragControl } from 'app/ui/molecules/form-controls/droppable'
  15. import { useTranslation } from 'react-i18next'
  16. function CourseStructure({ data }) {
  17. const { t } = useTranslation();
  18. const { setBottomSheetData } = useBottomSheetData();
  19. const isEditable = data.isEditable;
  20. const courseId = data.entry_id;
  21. const initialState = {
  22. action: null,
  23. modal: null,
  24. data: data,
  25. };
  26. const [state, dispatch] = useReducer(reducer, initialState);
  27. function reducer(state, action) {
  28. switch (action.type) {
  29. case 'SET_DATA':
  30. return {
  31. ...state,
  32. action: null,
  33. modal: null,
  34. data: action.data
  35. };
  36. default:
  37. return state;
  38. }
  39. }
  40. // Plain functions: only used from handlers/JSX, so identity does not matter
  41. // (React Compiler memoizes them). Declared in dependency order.
  42. const resetData = async () => {
  43. const response = await fetcher(`/api.php?r=bx_courses/entity_structure_l1_block/&params[]=${courseId}`);
  44. dispatch({ type: 'SET_DATA', data: response.data[0].data });
  45. };
  46. const handleUpdate = () => {
  47. setTimeout(() => {
  48. setBottomSheetData(false);
  49. resetData();
  50. }, 100);
  51. };
  52. const handleAddModule = async (event) => {
  53. event?.preventDefault?.();
  54. const fetchedData = await fetcher(`/api.php?r=system/perfom_action_api/TemplServiceGrid/&params[]=&o=bx_courses_cnt_structure_manage&a=add&parent_id=0&entry_id=${courseId}`);
  55. const content = { content: fetchedData.data, designbox_id: 0 };
  56. setBottomSheetData({ title: content.content[0]?.title || " ", content: <View className='px-1'><BlockByData onFormEmpty={handleUpdate} block={content} /></View> });
  57. };
  58. const handleManage = async (item, id) => {
  59. if (item.action == "edit") {
  60. const fetchedData = await fetcher(`/api.php?r=system/perfom_action_api/TemplServiceGrid/&params[]=&o=bx_courses_cnt_structure_manage&a=edit&parent_id=0&entry_id=${courseId}&id=${id}`);
  61. const content = { content: fetchedData.data, designbox_id: 0 };
  62. setBottomSheetData({ title: content.content[0]?.title || " ", content: <View className='px-1'><BlockByData onFormEmpty={handleUpdate} block={content} /></View> });
  63. }
  64. if (item.action == "delete") {
  65. await fetcher(`/api.php?r=system/perfom_action_api/TemplServiceGrid/&params[]=&o=bx_courses_cnt_structure_manage&a=delete&parent_id=0&entry_id=${courseId}&ids[]=${id}`);
  66. resetData();
  67. }
  68. };
  69. const handleModulesSort = async (result) => {
  70. if (!result.destination) return;
  71. const updatedData = [...state.data.items];
  72. const [removed] = updatedData.splice(result.source.index, 1);
  73. updatedData.splice(result.destination.index, 0, removed);
  74. updatedData.forEach((item, index) => {
  75. item.order = index;
  76. });
  77. dispatch({ type: 'SET_DATA', data: { ...state.data, items: updatedData }});
  78. await fetcher(`/api.php?r=system/perfom_action_api/TemplServiceGrid/&params[]=&o=bx_courses_cnt_structure_manage&a=reorder&parent_id=0&entry_id=${courseId}&` + updatedData.map(item => `bx_courses_cnt_structure_manage_row[]=${item.id}`).join('&'));
  79. };
  80. const renderModuleItem = (index, item, dragHandleProps) => {
  81. let icon = "Check";
  82. let color = "gray-600";
  83. let colorButton = "emerald-400";
  84. let colorButtonText = "emerald-400";
  85. if (item.status == "in process") {
  86. icon = "Hourglass";
  87. color = "red-400";
  88. colorButton = "white";
  89. colorButtonText = "red-400";
  90. }
  91. if (item.status == "not started") {
  92. icon = "Bookmark";
  93. color = "gray-400";
  94. colorButton = "gray-500";
  95. colorButtonText = "white";
  96. }
  97. const manageMenu = [
  98. { title: t('Edit module'), action: "edit" },
  99. { title: t('Delete module'), action: "delete" },
  100. ];
  101. return (
  102. <View className='m-2 w-72' key={item.index}>
  103. <Card rounded=' rounded-none sm:rounded-2xl ' margin={'bg-' + color + ' max-w-screen-lg mx-auto w-full p-3 sm:p-4 mb-1 sm:mb-4 '}>
  104. <Link href={item.link}>
  105. <View className={`mb-2 bg-${color}`}>
  106. {!isEditable && <Progress value={item.percent} />}
  107. <Row className='justify-between'>
  108. <View className='my-2 text-xs '><Text className="text-white">{t('Module {{index}}', { index: item.index })}</Text></View>
  109. {isEditable && <Row className='items-center justify-center gap-x-2'><DropdownMenu items={manageMenu} onSelect={(oItem) => { handleManage(oItem, item.id) }} buttonProps={{ controlSize: 'small', borderShape: 'circle', image: 'Cog', accessibilityLabel: t('Settings') }} /><DragControl dragHandleProps={dragHandleProps} accessibilityLabel={t('Reorder')} className="u-neo-btn-link hit-area-4"><NeoButton controlSize="small" borderShape="circle" image="ArrowLeftRight" interactive /></DragControl></Row>}
  110. </Row>
  111. <View className='h-12'>
  112. <Text className="text-white text-lg leading-tight tracking-tight font-bold text-secondary-foreground " numberOfLines={2}>{item.title}</Text>
  113. </View>
  114. </View>
  115. <Row className='gap-x-2 items-end mt-4'>
  116. <NeoButton controlSize="mini" borderShape="capsule" image={icon} label={item.status} />
  117. {item?.counters?.map((item2, index) => {
  118. return (
  119. <NeoButton key={`cnt-${index}`} controlSize="mini" borderShape="capsule" label={`${item2.cn_progress} ${item2.cn_title}`} />
  120. )
  121. })}
  122. </Row>
  123. </Link>
  124. </Card>
  125. </View>
  126. )
  127. }
  128. return (
  129. <DragContext onSort={handleModulesSort} renderItem={renderModuleItem} direction='horizontal'>
  130. <Scroll horizontal={true} step={250} className='w-full'>
  131. {
  132. state.data.items && state.data.items.map((item, index) => {
  133. return (
  134. <DragItem data={item} index={index} key={index} renderItem={renderModuleItem} isDragEnabled={isEditable} />
  135. )
  136. })
  137. }
  138. {isEditable && (<View className='m-2 w-72'>
  139. <Card rounded=' rounded-none sm:rounded-2xl ' >
  140. <View className='h-40 items-center justify-center'>
  141. <NeoButton controlSize="small" borderShape="capsule" image="Plus" label={t('Add new')} classNames={{ root: 'self-center' }} onPress={(event) => { handleAddModule(event, courseId) }} />
  142. </View>
  143. </Card>
  144. </View>)
  145. }
  146. </Scroll></DragContext>)
  147. }
  148. export default memo(CourseStructure);