diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..05e5a39
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,485 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = crlf
+indent_size = 4
+indent_style = space
+insert_final_newline = false
+max_line_length = 120
+tab_width = 4
+ij_continuation_indent_size = 4
+ij_formatter_off_tag = @formatter:off
+ij_formatter_on_tag = @formatter:on
+ij_formatter_tags_enabled = true
+ij_smart_tabs = false
+ij_visual_guides = none
+ij_wrap_on_typing = false
+
+[*.css]
+ij_css_align_closing_brace_with_properties = false
+ij_css_blank_lines_around_nested_selector = 1
+ij_css_blank_lines_between_blocks = 1
+ij_css_block_comment_add_space = false
+ij_css_brace_placement = end_of_line
+ij_css_enforce_quotes_on_format = false
+ij_css_hex_color_long_format = false
+ij_css_hex_color_lower_case = false
+ij_css_hex_color_short_format = false
+ij_css_hex_color_upper_case = false
+ij_css_keep_blank_lines_in_code = 2
+ij_css_keep_indents_on_empty_lines = false
+ij_css_keep_single_line_blocks = false
+ij_css_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow
+ij_css_space_after_colon = true
+ij_css_space_before_opening_brace = true
+ij_css_use_double_quotes = true
+ij_css_value_alignment = do_not_align
+
+[*.sass]
+indent_size = 2
+ij_sass_align_closing_brace_with_properties = false
+ij_sass_blank_lines_around_nested_selector = 1
+ij_sass_blank_lines_between_blocks = 1
+ij_sass_brace_placement = 0
+ij_sass_enforce_quotes_on_format = false
+ij_sass_hex_color_long_format = false
+ij_sass_hex_color_lower_case = false
+ij_sass_hex_color_short_format = false
+ij_sass_hex_color_upper_case = false
+ij_sass_keep_blank_lines_in_code = 2
+ij_sass_keep_indents_on_empty_lines = false
+ij_sass_keep_single_line_blocks = false
+ij_sass_line_comment_add_space = false
+ij_sass_line_comment_at_first_column = false
+ij_sass_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow
+ij_sass_space_after_colon = true
+ij_sass_space_before_opening_brace = true
+ij_sass_use_double_quotes = true
+ij_sass_value_alignment = 0
+
+[*.scss]
+indent_size = 2
+ij_scss_align_closing_brace_with_properties = false
+ij_scss_blank_lines_around_nested_selector = 1
+ij_scss_blank_lines_between_blocks = 1
+ij_scss_block_comment_add_space = false
+ij_scss_brace_placement = 0
+ij_scss_enforce_quotes_on_format = false
+ij_scss_hex_color_long_format = false
+ij_scss_hex_color_lower_case = false
+ij_scss_hex_color_short_format = false
+ij_scss_hex_color_upper_case = false
+ij_scss_keep_blank_lines_in_code = 2
+ij_scss_keep_indents_on_empty_lines = false
+ij_scss_keep_single_line_blocks = false
+ij_scss_line_comment_add_space = false
+ij_scss_line_comment_at_first_column = false
+ij_scss_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow
+ij_scss_space_after_colon = true
+ij_scss_space_before_opening_brace = true
+ij_scss_use_double_quotes = true
+ij_scss_value_alignment = 0
+
+[{*.ats,*.cts,*.mts,*.ts}]
+ij_continuation_indent_size = 4
+ij_typescript_align_imports = false
+ij_typescript_align_multiline_array_initializer_expression = false
+ij_typescript_align_multiline_binary_operation = false
+ij_typescript_align_multiline_chained_methods = false
+ij_typescript_align_multiline_extends_list = false
+ij_typescript_align_multiline_for = true
+ij_typescript_align_multiline_parameters = true
+ij_typescript_align_multiline_parameters_in_calls = false
+ij_typescript_align_multiline_ternary_operation = false
+ij_typescript_align_object_properties = 0
+ij_typescript_align_union_types = false
+ij_typescript_align_var_statements = 0
+ij_typescript_array_initializer_new_line_after_left_brace = false
+ij_typescript_array_initializer_right_brace_on_new_line = false
+ij_typescript_array_initializer_wrap = off
+ij_typescript_assignment_wrap = off
+ij_typescript_binary_operation_sign_on_next_line = false
+ij_typescript_binary_operation_wrap = off
+ij_typescript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/**
+ij_typescript_blank_lines_after_imports = 1
+ij_typescript_blank_lines_around_class = 1
+ij_typescript_blank_lines_around_field = 0
+ij_typescript_blank_lines_around_field_in_interface = 0
+ij_typescript_blank_lines_around_function = 1
+ij_typescript_blank_lines_around_method = 1
+ij_typescript_blank_lines_around_method_in_interface = 1
+ij_typescript_block_brace_style = end_of_line
+ij_typescript_block_comment_add_space = false
+ij_typescript_block_comment_at_first_column = true
+ij_typescript_call_parameters_new_line_after_left_paren = false
+ij_typescript_call_parameters_right_paren_on_new_line = false
+ij_typescript_call_parameters_wrap = off
+ij_typescript_catch_on_new_line = false
+ij_typescript_chained_call_dot_on_new_line = true
+ij_typescript_class_brace_style = end_of_line
+ij_typescript_comma_on_new_line = false
+ij_typescript_do_while_brace_force = never
+ij_typescript_else_on_new_line = false
+ij_typescript_enforce_trailing_comma = keep
+ij_typescript_enum_constants_wrap = on_every_item
+ij_typescript_extends_keyword_wrap = off
+ij_typescript_extends_list_wrap = off
+ij_typescript_field_prefix = _
+ij_typescript_file_name_style = relaxed
+ij_typescript_finally_on_new_line = false
+ij_typescript_for_brace_force = never
+ij_typescript_for_statement_new_line_after_left_paren = false
+ij_typescript_for_statement_right_paren_on_new_line = false
+ij_typescript_for_statement_wrap = off
+ij_typescript_force_quote_style = false
+ij_typescript_force_semicolon_style = false
+ij_typescript_function_expression_brace_style = end_of_line
+ij_typescript_if_brace_force = never
+ij_typescript_import_merge_members = global
+ij_typescript_import_prefer_absolute_path = global
+ij_typescript_import_sort_members = true
+ij_typescript_import_sort_module_name = false
+ij_typescript_import_use_node_resolution = true
+ij_typescript_imports_wrap = on_every_item
+ij_typescript_indent_case_from_switch = true
+ij_typescript_indent_chained_calls = true
+ij_typescript_indent_package_children = 0
+ij_typescript_jsdoc_include_types = false
+ij_typescript_jsx_attribute_value = braces
+ij_typescript_keep_blank_lines_in_code = 2
+ij_typescript_keep_first_column_comment = true
+ij_typescript_keep_indents_on_empty_lines = false
+ij_typescript_keep_line_breaks = true
+ij_typescript_keep_simple_blocks_in_one_line = false
+ij_typescript_keep_simple_methods_in_one_line = false
+ij_typescript_line_comment_add_space = true
+ij_typescript_line_comment_at_first_column = false
+ij_typescript_method_brace_style = end_of_line
+ij_typescript_method_call_chain_wrap = off
+ij_typescript_method_parameters_new_line_after_left_paren = false
+ij_typescript_method_parameters_right_paren_on_new_line = false
+ij_typescript_method_parameters_wrap = off
+ij_typescript_object_literal_wrap = on_every_item
+ij_typescript_object_types_wrap = on_every_item
+ij_typescript_parentheses_expression_new_line_after_left_paren = false
+ij_typescript_parentheses_expression_right_paren_on_new_line = false
+ij_typescript_place_assignment_sign_on_next_line = false
+ij_typescript_prefer_as_type_cast = false
+ij_typescript_prefer_explicit_types_function_expression_returns = false
+ij_typescript_prefer_explicit_types_function_returns = false
+ij_typescript_prefer_explicit_types_vars_fields = false
+ij_typescript_prefer_parameters_wrap = false
+ij_typescript_reformat_c_style_comments = false
+ij_typescript_space_after_colon = true
+ij_typescript_space_after_comma = true
+ij_typescript_space_after_dots_in_rest_parameter = false
+ij_typescript_space_after_generator_mult = true
+ij_typescript_space_after_property_colon = true
+ij_typescript_space_after_quest = true
+ij_typescript_space_after_type_colon = true
+ij_typescript_space_after_unary_not = false
+ij_typescript_space_before_async_arrow_lparen = true
+ij_typescript_space_before_catch_keyword = true
+ij_typescript_space_before_catch_left_brace = true
+ij_typescript_space_before_catch_parentheses = true
+ij_typescript_space_before_class_lbrace = true
+ij_typescript_space_before_class_left_brace = true
+ij_typescript_space_before_colon = true
+ij_typescript_space_before_comma = false
+ij_typescript_space_before_do_left_brace = true
+ij_typescript_space_before_else_keyword = true
+ij_typescript_space_before_else_left_brace = true
+ij_typescript_space_before_finally_keyword = true
+ij_typescript_space_before_finally_left_brace = true
+ij_typescript_space_before_for_left_brace = true
+ij_typescript_space_before_for_parentheses = true
+ij_typescript_space_before_for_semicolon = false
+ij_typescript_space_before_function_left_parenth = true
+ij_typescript_space_before_generator_mult = false
+ij_typescript_space_before_if_left_brace = true
+ij_typescript_space_before_if_parentheses = true
+ij_typescript_space_before_method_call_parentheses = false
+ij_typescript_space_before_method_left_brace = true
+ij_typescript_space_before_method_parentheses = false
+ij_typescript_space_before_property_colon = false
+ij_typescript_space_before_quest = true
+ij_typescript_space_before_switch_left_brace = true
+ij_typescript_space_before_switch_parentheses = true
+ij_typescript_space_before_try_left_brace = true
+ij_typescript_space_before_type_colon = false
+ij_typescript_space_before_unary_not = false
+ij_typescript_space_before_while_keyword = true
+ij_typescript_space_before_while_left_brace = true
+ij_typescript_space_before_while_parentheses = true
+ij_typescript_spaces_around_additive_operators = true
+ij_typescript_spaces_around_arrow_function_operator = true
+ij_typescript_spaces_around_assignment_operators = true
+ij_typescript_spaces_around_bitwise_operators = true
+ij_typescript_spaces_around_equality_operators = true
+ij_typescript_spaces_around_logical_operators = true
+ij_typescript_spaces_around_multiplicative_operators = true
+ij_typescript_spaces_around_relational_operators = true
+ij_typescript_spaces_around_shift_operators = true
+ij_typescript_spaces_around_unary_operator = false
+ij_typescript_spaces_within_array_initializer_brackets = false
+ij_typescript_spaces_within_brackets = false
+ij_typescript_spaces_within_catch_parentheses = false
+ij_typescript_spaces_within_for_parentheses = false
+ij_typescript_spaces_within_if_parentheses = false
+ij_typescript_spaces_within_imports = false
+ij_typescript_spaces_within_interpolation_expressions = false
+ij_typescript_spaces_within_method_call_parentheses = false
+ij_typescript_spaces_within_method_parentheses = false
+ij_typescript_spaces_within_object_literal_braces = false
+ij_typescript_spaces_within_object_type_braces = true
+ij_typescript_spaces_within_parentheses = false
+ij_typescript_spaces_within_switch_parentheses = false
+ij_typescript_spaces_within_type_assertion = false
+ij_typescript_spaces_within_union_types = true
+ij_typescript_spaces_within_while_parentheses = false
+ij_typescript_special_else_if_treatment = true
+ij_typescript_ternary_operation_signs_on_next_line = false
+ij_typescript_ternary_operation_wrap = off
+ij_typescript_union_types_wrap = on_every_item
+ij_typescript_use_chained_calls_group_indents = false
+ij_typescript_use_double_quotes = true
+ij_typescript_use_explicit_js_extension = auto
+ij_typescript_use_path_mapping = always
+ij_typescript_use_public_modifier = false
+ij_typescript_use_semicolon_after_statement = true
+ij_typescript_var_declaration_wrap = normal
+ij_typescript_while_brace_force = never
+ij_typescript_while_on_new_line = false
+ij_typescript_wrap_comments = false
+
+[{*.cjs,*.js}]
+ij_continuation_indent_size = 4
+ij_javascript_align_imports = false
+ij_javascript_align_multiline_array_initializer_expression = false
+ij_javascript_align_multiline_binary_operation = false
+ij_javascript_align_multiline_chained_methods = false
+ij_javascript_align_multiline_extends_list = false
+ij_javascript_align_multiline_for = true
+ij_javascript_align_multiline_parameters = true
+ij_javascript_align_multiline_parameters_in_calls = false
+ij_javascript_align_multiline_ternary_operation = false
+ij_javascript_align_object_properties = 0
+ij_javascript_align_union_types = false
+ij_javascript_align_var_statements = 0
+ij_javascript_array_initializer_new_line_after_left_brace = false
+ij_javascript_array_initializer_right_brace_on_new_line = false
+ij_javascript_array_initializer_wrap = off
+ij_javascript_assignment_wrap = off
+ij_javascript_binary_operation_sign_on_next_line = false
+ij_javascript_binary_operation_wrap = off
+ij_javascript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/**
+ij_javascript_blank_lines_after_imports = 1
+ij_javascript_blank_lines_around_class = 1
+ij_javascript_blank_lines_around_field = 0
+ij_javascript_blank_lines_around_function = 1
+ij_javascript_blank_lines_around_method = 1
+ij_javascript_block_brace_style = end_of_line
+ij_javascript_block_comment_add_space = false
+ij_javascript_block_comment_at_first_column = true
+ij_javascript_call_parameters_new_line_after_left_paren = false
+ij_javascript_call_parameters_right_paren_on_new_line = false
+ij_javascript_call_parameters_wrap = off
+ij_javascript_catch_on_new_line = false
+ij_javascript_chained_call_dot_on_new_line = true
+ij_javascript_class_brace_style = end_of_line
+ij_javascript_comma_on_new_line = false
+ij_javascript_do_while_brace_force = never
+ij_javascript_else_on_new_line = false
+ij_javascript_enforce_trailing_comma = keep
+ij_javascript_extends_keyword_wrap = off
+ij_javascript_extends_list_wrap = off
+ij_javascript_field_prefix = _
+ij_javascript_file_name_style = relaxed
+ij_javascript_finally_on_new_line = false
+ij_javascript_for_brace_force = never
+ij_javascript_for_statement_new_line_after_left_paren = false
+ij_javascript_for_statement_right_paren_on_new_line = false
+ij_javascript_for_statement_wrap = off
+ij_javascript_force_quote_style = false
+ij_javascript_force_semicolon_style = false
+ij_javascript_function_expression_brace_style = end_of_line
+ij_javascript_if_brace_force = never
+ij_javascript_import_merge_members = global
+ij_javascript_import_prefer_absolute_path = global
+ij_javascript_import_sort_members = true
+ij_javascript_import_sort_module_name = false
+ij_javascript_import_use_node_resolution = true
+ij_javascript_imports_wrap = on_every_item
+ij_javascript_indent_case_from_switch = true
+ij_javascript_indent_chained_calls = true
+ij_javascript_indent_package_children = 0
+ij_javascript_jsx_attribute_value = braces
+ij_javascript_keep_blank_lines_in_code = 2
+ij_javascript_keep_first_column_comment = true
+ij_javascript_keep_indents_on_empty_lines = false
+ij_javascript_keep_line_breaks = true
+ij_javascript_keep_simple_blocks_in_one_line = false
+ij_javascript_keep_simple_methods_in_one_line = false
+ij_javascript_line_comment_add_space = true
+ij_javascript_line_comment_at_first_column = false
+ij_javascript_method_brace_style = end_of_line
+ij_javascript_method_call_chain_wrap = off
+ij_javascript_method_parameters_new_line_after_left_paren = false
+ij_javascript_method_parameters_right_paren_on_new_line = false
+ij_javascript_method_parameters_wrap = off
+ij_javascript_object_literal_wrap = on_every_item
+ij_javascript_object_types_wrap = on_every_item
+ij_javascript_parentheses_expression_new_line_after_left_paren = false
+ij_javascript_parentheses_expression_right_paren_on_new_line = false
+ij_javascript_place_assignment_sign_on_next_line = false
+ij_javascript_prefer_as_type_cast = false
+ij_javascript_prefer_explicit_types_function_expression_returns = false
+ij_javascript_prefer_explicit_types_function_returns = false
+ij_javascript_prefer_explicit_types_vars_fields = false
+ij_javascript_prefer_parameters_wrap = false
+ij_javascript_reformat_c_style_comments = false
+ij_javascript_space_after_colon = true
+ij_javascript_space_after_comma = true
+ij_javascript_space_after_dots_in_rest_parameter = false
+ij_javascript_space_after_generator_mult = true
+ij_javascript_space_after_property_colon = true
+ij_javascript_space_after_quest = true
+ij_javascript_space_after_type_colon = true
+ij_javascript_space_after_unary_not = false
+ij_javascript_space_before_async_arrow_lparen = true
+ij_javascript_space_before_catch_keyword = true
+ij_javascript_space_before_catch_left_brace = true
+ij_javascript_space_before_catch_parentheses = true
+ij_javascript_space_before_class_lbrace = true
+ij_javascript_space_before_class_left_brace = true
+ij_javascript_space_before_colon = true
+ij_javascript_space_before_comma = false
+ij_javascript_space_before_do_left_brace = true
+ij_javascript_space_before_else_keyword = true
+ij_javascript_space_before_else_left_brace = true
+ij_javascript_space_before_finally_keyword = true
+ij_javascript_space_before_finally_left_brace = true
+ij_javascript_space_before_for_left_brace = true
+ij_javascript_space_before_for_parentheses = true
+ij_javascript_space_before_for_semicolon = false
+ij_javascript_space_before_function_left_parenth = true
+ij_javascript_space_before_generator_mult = false
+ij_javascript_space_before_if_left_brace = true
+ij_javascript_space_before_if_parentheses = true
+ij_javascript_space_before_method_call_parentheses = false
+ij_javascript_space_before_method_left_brace = true
+ij_javascript_space_before_method_parentheses = false
+ij_javascript_space_before_property_colon = false
+ij_javascript_space_before_quest = true
+ij_javascript_space_before_switch_left_brace = true
+ij_javascript_space_before_switch_parentheses = true
+ij_javascript_space_before_try_left_brace = true
+ij_javascript_space_before_type_colon = false
+ij_javascript_space_before_unary_not = false
+ij_javascript_space_before_while_keyword = true
+ij_javascript_space_before_while_left_brace = true
+ij_javascript_space_before_while_parentheses = true
+ij_javascript_spaces_around_additive_operators = true
+ij_javascript_spaces_around_arrow_function_operator = true
+ij_javascript_spaces_around_assignment_operators = true
+ij_javascript_spaces_around_bitwise_operators = true
+ij_javascript_spaces_around_equality_operators = true
+ij_javascript_spaces_around_logical_operators = true
+ij_javascript_spaces_around_multiplicative_operators = true
+ij_javascript_spaces_around_relational_operators = true
+ij_javascript_spaces_around_shift_operators = true
+ij_javascript_spaces_around_unary_operator = false
+ij_javascript_spaces_within_array_initializer_brackets = false
+ij_javascript_spaces_within_brackets = false
+ij_javascript_spaces_within_catch_parentheses = false
+ij_javascript_spaces_within_for_parentheses = false
+ij_javascript_spaces_within_if_parentheses = false
+ij_javascript_spaces_within_imports = false
+ij_javascript_spaces_within_interpolation_expressions = false
+ij_javascript_spaces_within_method_call_parentheses = false
+ij_javascript_spaces_within_method_parentheses = false
+ij_javascript_spaces_within_object_literal_braces = false
+ij_javascript_spaces_within_object_type_braces = true
+ij_javascript_spaces_within_parentheses = false
+ij_javascript_spaces_within_switch_parentheses = false
+ij_javascript_spaces_within_type_assertion = false
+ij_javascript_spaces_within_union_types = true
+ij_javascript_spaces_within_while_parentheses = false
+ij_javascript_special_else_if_treatment = true
+ij_javascript_ternary_operation_signs_on_next_line = false
+ij_javascript_ternary_operation_wrap = off
+ij_javascript_union_types_wrap = on_every_item
+ij_javascript_use_chained_calls_group_indents = false
+ij_javascript_use_double_quotes = true
+ij_javascript_use_explicit_js_extension = auto
+ij_javascript_use_path_mapping = always
+ij_javascript_use_public_modifier = false
+ij_javascript_use_semicolon_after_statement = true
+ij_javascript_var_declaration_wrap = normal
+ij_javascript_while_brace_force = never
+ij_javascript_while_on_new_line = false
+ij_javascript_wrap_comments = false
+
+[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}]
+indent_size = 2
+ij_json_array_wrapping = split_into_lines
+ij_json_keep_blank_lines_in_code = 0
+ij_json_keep_indents_on_empty_lines = false
+ij_json_keep_line_breaks = true
+ij_json_keep_trailing_comma = false
+ij_json_object_wrapping = split_into_lines
+ij_json_property_alignment = do_not_align
+ij_json_space_after_colon = true
+ij_json_space_after_comma = true
+ij_json_space_before_colon = false
+ij_json_space_before_comma = false
+ij_json_spaces_within_braces = false
+ij_json_spaces_within_brackets = false
+ij_json_wrap_long_lines = false
+
+[{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}]
+ij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3
+ij_html_align_attributes = true
+ij_html_align_text = false
+ij_html_attribute_wrap = normal
+ij_html_block_comment_add_space = false
+ij_html_block_comment_at_first_column = true
+ij_html_do_not_align_children_of_min_lines = 0
+ij_html_do_not_break_if_inline_tags = title, h1, h2, h3, h4, h5, h6, p
+ij_html_do_not_indent_children_of_tags = html, body, thead, tbody, tfoot
+ij_html_enforce_quotes = false
+ij_html_inline_tags = a, abbr, acronym, b, basefont, bdo, big, br, cite, cite, code, dfn, em, font, i, img, input, kbd, label, q, s, samp, select, small, span, strike, strong, sub, sup, textarea, tt, u, var
+ij_html_keep_blank_lines = 2
+ij_html_keep_indents_on_empty_lines = false
+ij_html_keep_line_breaks = true
+ij_html_keep_line_breaks_in_text = true
+ij_html_keep_whitespaces = false
+ij_html_keep_whitespaces_inside = span, pre, textarea
+ij_html_line_comment_at_first_column = true
+ij_html_new_line_after_last_attribute = never
+ij_html_new_line_before_first_attribute = never
+ij_html_quote_style = double
+ij_html_remove_new_line_before_tags = br
+ij_html_space_after_tag_name = false
+ij_html_space_around_equality_in_attribute = false
+ij_html_space_inside_empty_tag = false
+ij_html_text_wrap = normal
+
+[{*.markdown,*.md}]
+ij_markdown_force_one_space_after_blockquote_symbol = true
+ij_markdown_force_one_space_after_header_symbol = true
+ij_markdown_force_one_space_after_list_bullet = true
+ij_markdown_force_one_space_between_words = true
+ij_markdown_format_tables = true
+ij_markdown_insert_quote_arrows_on_wrap = true
+ij_markdown_keep_indents_on_empty_lines = false
+ij_markdown_keep_line_breaks_inside_text_blocks = true
+ij_markdown_max_lines_around_block_elements = 1
+ij_markdown_max_lines_around_header = 1
+ij_markdown_max_lines_between_paragraphs = 1
+ij_markdown_min_lines_around_block_elements = 1
+ij_markdown_min_lines_around_header = 1
+ij_markdown_min_lines_between_paragraphs = 1
+ij_markdown_wrap_text_if_long = true
+ij_markdown_wrap_text_inside_blockquotes = true
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..cb6d4e4
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,5 @@
+{
+ "extends": [
+ "next/core-web-vitals"
+ ]
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1e62a6d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,34 @@
+# Logs
+logs
+*.log
+
+# Runtime data
+pids
+*.pid
+*.seed
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directory
+node_modules
+cypress/videos
+cypress/screenshots
+.next
+.env*
+.env
+.idea
+package-lock.json
+ecosystem.config.js
\ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..fd06551
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,118 @@
+image: node:latest
+
+stages:
+ - build
+ - test
+ - test:build
+ - deploy
+
+merge:build:
+ stage: build
+ script:
+ - npm install
+ - cp example.env.local .env.local
+ - cp example.env.local .env.test
+ only:
+ - merge_requests
+ artifacts:
+ paths:
+ - node_modules
+ - .env.local
+ - .env.test
+
+merge:test:jest:
+ stage: test
+ script:
+ - npm run test
+ only:
+ - merge_requests
+ artifacts:
+ paths:
+ - node_modules
+ - .env.local
+ - .env.test
+
+merge:test:lint:
+ stage: test
+ script:
+ - npm run lint
+ only:
+ - merge_requests
+ artifacts:
+ paths:
+ - node_modules
+ - .env.local
+ - .env.test
+
+#merge:test:build:
+# stage: test:build
+# script:
+# - npm run build
+# only:
+# - merge_requests
+# artifacts:
+# paths:
+# - node_modules
+# - .env.local
+# - .env.test
+
+build:
+ stage: build
+ script:
+ - npm install
+ - cp example.env.local .env.local
+ - cp example.env.local .env.test
+ only:
+ - develop
+ - main
+ artifacts:
+ paths:
+ - node_modules
+ - .env.local
+ - .env.test
+
+test:jest:
+ stage: test
+ script:
+ - npm run test
+ only:
+ - develop
+ - main
+ artifacts:
+ paths:
+ - node_modules
+ - .env.local
+ - .env.test
+
+test:lint:
+ stage: test
+ script:
+ - npm run lint
+ only:
+ - develop
+ - main
+ artifacts:
+ paths:
+ - node_modules
+ - .env.local
+ - .env.test
+
+#test:build:
+# stage: test:build
+# script:
+# - npm run build
+# only:
+# - develop
+# - main
+# artifacts:
+# paths:
+# - node_modules
+# - .env.local
+# - .env.test
+
+deploy:
+ stage: deploy
+ script:
+ - echo "Run deploy project"
+ only:
+ - main
\ No newline at end of file
diff --git a/cypress.config.js b/cypress.config.js
new file mode 100644
index 0000000..3b67130
--- /dev/null
+++ b/cypress.config.js
@@ -0,0 +1,16 @@
+const { defineConfig } = require("cypress");
+
+module.exports = defineConfig({
+ e2e: {
+ setupNodeEvents(on, config) {
+ // implement node event listeners here
+ },
+ },
+
+ component: {
+ devServer: {
+ framework: "next",
+ bundler: "webpack",
+ },
+ },
+});
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
new file mode 100644
index 0000000..66ea16e
--- /dev/null
+++ b/cypress/support/commands.js
@@ -0,0 +1,25 @@
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add('login', (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
\ No newline at end of file
diff --git a/cypress/support/component-index.html b/cypress/support/component-index.html
new file mode 100644
index 0000000..3e16e9b
--- /dev/null
+++ b/cypress/support/component-index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Components App
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cypress/support/component.js b/cypress/support/component.js
new file mode 100644
index 0000000..8f9154b
--- /dev/null
+++ b/cypress/support/component.js
@@ -0,0 +1,27 @@
+// ***********************************************************
+// This example support/component.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
+
+import { mount } from 'cypress/react18'
+
+Cypress.Commands.add('mount', mount)
+
+// Example use:
+// cy.mount()
\ No newline at end of file
diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js
new file mode 100644
index 0000000..0e7290a
--- /dev/null
+++ b/cypress/support/e2e.js
@@ -0,0 +1,20 @@
+// ***********************************************************
+// This example support/e2e.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
\ No newline at end of file
diff --git a/ecosystem.config.js.example b/ecosystem.config.js.example
new file mode 100644
index 0000000..6847796
--- /dev/null
+++ b/ecosystem.config.js.example
@@ -0,0 +1,18 @@
+module.exports = {
+ apps: [{
+ name: "crm_app",
+ script: 'node_modules/next/dist/bin/next', // cluster mode run with node only, not npm
+ args: 'start -p 3003',
+ exec_mode: "cluster", // default fork
+ instances: "10",
+ kill_timeout: 4000,
+ wait_ready: true,
+ autorestart: true,
+ watch: false,
+ max_memory_restart: "1G",
+ log_date_format: "YYYY-MM-DD HH:mm Z",
+ env_prod: {
+ APP_ENV: 'prod' // APP_ENV=prod
+ }
+ }],
+};
\ No newline at end of file
diff --git a/example.env.local b/example.env.local
new file mode 100644
index 0000000..cc363e7
--- /dev/null
+++ b/example.env.local
@@ -0,0 +1,17 @@
+NEXT_PUBLIC_API_NAME = "CRM Dashboard"
+NEXT_PUBLIC_API_VERSION = "1.21.6"
+NEXT_PUBLIC_DEFAULT_LANGUAGE = "fa"
+NEXT_PUBLIC_DEFAULT_DIRECTION = "rtl"
+
+NEXT_PUBLIC_PRIMARY_MAIN = "#1b4332"
+NEXT_PUBLIC_SECONDARY_MAIN = "#800e13"
+
+NEXT_PUBLIC_BASE_URL = "https://crm.witel.ir"
+NEXT_PUBLIC_SERVER_SOCKET_URL = "wss://crmws.witel.ir"
+NEXT_PUBLIC_POWERED_BY_URL = "https://witel.ir"
+
+#["NEXT_PUBLIC_HAS_WIDGET" , "NEXT_PUBLIC_HAS_NOTIFICATION"]
+NEXT_PUBLIC_HAS_VALUE=["NEXT_PUBLIC_HAS_WIDGET"]
+
+NODE_ENV = "development"
+
diff --git a/jest.config.mjs b/jest.config.mjs
new file mode 100644
index 0000000..5bda02f
--- /dev/null
+++ b/jest.config.mjs
@@ -0,0 +1,17 @@
+import nextJest from 'next/jest.js'
+
+const createJestConfig = nextJest({
+ // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
+ dir: './',
+})
+
+// Add any custom config to be passed to Jest
+/** @type {import('jest').Config} */
+const config = {
+ // Add more setup options before each test is run
+ setupFilesAfterEnv: ['/jest.setup.js'],
+ testEnvironment: 'jest-environment-jsdom',
+}
+
+// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
+export default createJestConfig(config)
\ No newline at end of file
diff --git a/jest.setup.js b/jest.setup.js
new file mode 100644
index 0000000..fadd766
--- /dev/null
+++ b/jest.setup.js
@@ -0,0 +1,23 @@
+import '@testing-library/jest-dom'
+import {server} from "./mocks/server";
+import mockRouter from 'next-router-mock'
+
+jest.mock('next/router', () => jest.requireActual('next-router-mock'))
+
+beforeAll(() => {
+ server.listen()
+})
+beforeEach(() => {
+ localStorage.clear();
+ mockRouter.query = {}
+
+})
+
+afterEach(() => {
+ server.resetHandlers()
+})
+
+afterAll(() => {
+ server.close()
+})
+
diff --git a/jsconfig.json b/jsconfig.json
new file mode 100644
index 0000000..fcd02aa
--- /dev/null
+++ b/jsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ],
+ "&/*": [
+ "./public/*"
+ ]
+ }
+ }
+}
diff --git a/mocks/AppWithProvider.jsx b/mocks/AppWithProvider.jsx
new file mode 100644
index 0000000..a81bb3c
--- /dev/null
+++ b/mocks/AppWithProvider.jsx
@@ -0,0 +1,13 @@
+import App from "@/pages/_app";
+import fa from "&/locales/fa/app.json"
+
+const translations = {fa};
+const MockAppWithProviders = ({children, locale, title, isBot}) => {
+ const pageProps = {
+ title: title || '', isBot: isBot || true, locale: locale || 'fa', messages: translations[locale || 'fa']
+ }
+
+ return ( (<>{children}>)} pageProps={pageProps}/>)
+}
+
+export default MockAppWithProviders
\ No newline at end of file
diff --git a/mocks/handler.js b/mocks/handler.js
new file mode 100644
index 0000000..232c91e
--- /dev/null
+++ b/mocks/handler.js
@@ -0,0 +1,15 @@
+import { userHandler } from "./handlers/user";
+import { permissionsHandler } from "./handlers/permissions";
+import { rolesHandler } from "./handlers/roles";
+import { provincesHandler } from "./handlers/provinces";
+import { expertsHandler } from "./handlers/experts";
+import { categoriesHandler } from "./handlers/categories";
+
+export const handler = [
+ ...userHandler,
+ ...permissionsHandler,
+ ...rolesHandler,
+ ...provincesHandler,
+ ...expertsHandler,
+ ...categoriesHandler,
+];
diff --git a/mocks/handlers/categories.js b/mocks/handlers/categories.js
new file mode 100644
index 0000000..6864331
--- /dev/null
+++ b/mocks/handlers/categories.js
@@ -0,0 +1,25 @@
+import { rest } from "msw";
+import { GET_CATEGORY } from "@/core/data/apiRoutes";
+
+export const categoriesHandler = [
+ rest.get(GET_CATEGORY, (req, res, ctx) => {
+ return res(
+ ctx.json({
+ data: [
+ {
+ category_id: 1,
+ category_name: "اطلاعات راه",
+ subcategory_id: 1,
+ subcategory_name: "اطلاعات باز و بسته",
+ },
+ {
+ category_id: 3,
+ category_name: "سایر",
+ subcategory_id: 2,
+ subcategory_name: "مزاحم",
+ },
+ ],
+ })
+ );
+ }),
+];
diff --git a/mocks/handlers/experts.js b/mocks/handlers/experts.js
new file mode 100644
index 0000000..1bf2a31
--- /dev/null
+++ b/mocks/handlers/experts.js
@@ -0,0 +1,20 @@
+import {rest} from "msw";
+import {ADD_EXPERT, DELETE_EXPERT, UPDATE_EXPERT} from "@/core/data/apiRoutes";
+
+export const expertsHandler = [
+ rest.post(ADD_EXPERT, (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ );
+ }),
+ rest.delete(`${DELETE_EXPERT}/:id`, (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ );
+ }),
+ rest.post(`${UPDATE_EXPERT}/:id`, (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ );
+ }),
+]
\ No newline at end of file
diff --git a/mocks/handlers/permissions.js b/mocks/handlers/permissions.js
new file mode 100644
index 0000000..dfe792b
--- /dev/null
+++ b/mocks/handlers/permissions.js
@@ -0,0 +1,23 @@
+import {rest} from "msw";
+import {GET_PERMISSIONS_LIST} from "@/core/data/apiRoutes";
+
+export const permissionsHandler = [
+ rest.get(GET_PERMISSIONS_LIST, (req, res, ctx) => {
+ return res(ctx.json(
+ {
+ data: [
+ {
+ id: 1,
+ name: "manage_passenger_office_navgan",
+ name_fa: "مدیریت کارتابل رییس اداره مسافری استان"
+ },
+ {
+ id: 2,
+ name: "manage_province_working_group_navgan",
+ name_fa: "مدیریت کارتابل کارگروه استانی"
+ }
+ ]
+ }
+ ))
+ })
+]
\ No newline at end of file
diff --git a/mocks/handlers/provinces.js b/mocks/handlers/provinces.js
new file mode 100644
index 0000000..9065c12
--- /dev/null
+++ b/mocks/handlers/provinces.js
@@ -0,0 +1,19 @@
+import {rest} from "msw";
+import {GET_PROVINCE_LIST} from "@/core/data/apiRoutes";
+
+export const provincesHandler = [
+ rest.get(GET_PROVINCE_LIST, (req, res, ctx) => {
+ return res(ctx.json({
+ data: [
+ {
+ id: 1,
+ name: "آذربایجان شرقی"
+ },
+ {
+ id: 3,
+ name: "اردبیل"
+ }
+ ]
+ }))
+ }),
+]
\ No newline at end of file
diff --git a/mocks/handlers/roles.js b/mocks/handlers/roles.js
new file mode 100644
index 0000000..1c26ef5
--- /dev/null
+++ b/mocks/handlers/roles.js
@@ -0,0 +1,70 @@
+import {rest} from "msw";
+import {GET_ROLE_LIST, GET_ROLES} from "@/core/data/apiRoutes";
+
+export const rolesHandler = [
+ rest.get(GET_ROLES, (req, res, ctx) => {
+ return res(
+ ctx.json(
+ {
+ data: [
+ {
+ created_at: "2023-10-01T07:20:07.000000Z",
+ guard_name: "api",
+ id: 1,
+ name: "admin",
+ name_fa: "ادمین",
+ permissions: [
+ {
+ id: 1,
+ name: "manage_users",
+ },
+ {
+ id: 2,
+ name: "manage_roles",
+ },
+ ],
+ updated_at: "2023-10-10T07:38:12.000000Z"
+ },
+ {
+ created_at: "2023-10-01T07:20:07.000000Z",
+ guard_name: "api",
+ id: 2,
+ name: "manager",
+ name_fa: "مدیر",
+ permissions: [
+ {
+ id: 1,
+ name: "manage_users",
+ },
+ {
+ id: 2,
+ name: "manage_roles",
+ },
+ ],
+ updated_at: "2023-10-10T07:38:12.000000Z"
+ }
+ ],
+ meta: {
+ totalRowCount: 2
+ }
+ }
+ ),
+ );
+ }),
+ rest.get(GET_ROLE_LIST, (req, res, ctx) => {
+ return res(ctx.json({
+ data: [
+ {
+ id: 1,
+ name: "admin",
+ name_fa: "ادمین"
+ },
+ {
+ id: 2,
+ name: "manager",
+ name_fa: "مدیر"
+ }
+ ]
+ }))
+ }),
+]
\ No newline at end of file
diff --git a/mocks/handlers/user.js b/mocks/handlers/user.js
new file mode 100644
index 0000000..7e84703
--- /dev/null
+++ b/mocks/handlers/user.js
@@ -0,0 +1,29 @@
+import {rest} from "msw";
+import {GET_SIDEBAR_NOTIFICATION, GET_USER, SET_USER_PASSWORD} from "@/core/data/apiRoutes";
+
+export const userHandler = [
+ rest.get(GET_USER, (req, res, ctx) => {
+ return res(ctx.json({
+ data:{
+ id: 10,
+ full_name: "Witel Company",
+ position: "Software Engineer",
+ permissions:[
+ "manage_users",
+ "manage_roles",
+ "manage_boss"
+ ],
+ }
+ }))
+ }),
+ rest.get(GET_SIDEBAR_NOTIFICATION, (req, res, ctx) => {
+ return res(ctx.json({
+ data: []
+ }))
+ }),
+ rest.post(SET_USER_PASSWORD, (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ );
+ }),
+]
\ No newline at end of file
diff --git a/mocks/server.js b/mocks/server.js
new file mode 100644
index 0000000..0bf9e08
--- /dev/null
+++ b/mocks/server.js
@@ -0,0 +1,4 @@
+import {setupServer} from "msw/node";
+import {handler} from "./handler";
+
+export const server = setupServer(...handler)
\ No newline at end of file
diff --git a/next.config.js b/next.config.js
new file mode 100644
index 0000000..a288e34
--- /dev/null
+++ b/next.config.js
@@ -0,0 +1,12 @@
+/** @type {import('next').NextConfig} */
+
+const nextConfig = {
+ reactStrictMode: true,
+ i18n: {
+ defaultLocale: 'fa',
+ locales: ['fa'],
+ localeDetection: false,
+ }
+}
+
+module.exports = nextConfig
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f82c9f2
--- /dev/null
+++ b/package.json
@@ -0,0 +1,70 @@
+{
+ "name": "dashboard",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "publish-3002": "next start -p 3002",
+ "lint": "next lint",
+ "update": "run-script-os",
+ "update:win32": "echo 'ok'",
+ "update:linux": "rm -rf .next/ && npm i && npm run build && sudo systemctl restart crm.service",
+ "test": "jest",
+ "test:watch": "jest --watchAll",
+ "cypress:open": "cypress open",
+ "cypress:run": "cypress run"
+ },
+ "dependencies": {
+ "@emotion/react": "^11.10.6",
+ "@emotion/server": "^11.10.0",
+ "@emotion/styled": "^11.10.6",
+ "@mui/icons-material": "^5.11.16",
+ "@mui/material": "^5.12.0",
+ "@mui/x-date-pickers": "^6.9.2",
+ "axios": "^1.4.0",
+ "colord": "^2.9.3",
+ "date-fns-jalali": "^2.13.0-0",
+ "dayjs": "^1.11.9",
+ "dotenv": "^16.3.1",
+ "eslint": "8.36.0",
+ "formik": "^2.2.9",
+ "fs-extra": "^11.1.1",
+ "image-resize": "^1.3.2",
+ "image-to-file-converter": "^1.0.6",
+ "jalali-moment": "^3.3.11",
+ "material-react-table": "^1.11.1",
+ "moment": "^2.29.4",
+ "moment-jalaali": "^0.10.0",
+ "next": "^13.3.0",
+ "next-intl": "^2.13.1",
+ "next-useragent": "^2.8.0",
+ "nextjs-progressbar": "^0.0.16",
+ "prop-types": "^15.8.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-toastify": "^9.1.3",
+ "sass": "^1.62.0",
+ "socket.io-client": "^4.7.2",
+ "stylis": "^4.1.3",
+ "stylis-plugin-rtl": "^2.1.1",
+ "swr": "^2.1.5",
+ "yup": "^1.1.1"
+ },
+ "devDependencies": {
+ "@faker-js/faker": "^7.6.0",
+ "@testing-library/jest-dom": "^6.1.3",
+ "@testing-library/react": "^14.0.0",
+ "@testing-library/user-event": "^14.5.1",
+ "cypress": "^13.2.0",
+ "eslint-config-next": "^13.3.0",
+ "eslint-plugin-jest-dom": "^5.1.0",
+ "eslint-plugin-testing-library": "^6.0.1",
+ "jest": "^29.7.0",
+ "jest-environment-jsdom": "^29.7.0",
+ "msw": "^1.3.1",
+ "next-router-mock": "^0.9.10",
+ "run-script-os": "^1.1.6"
+ }
+}
diff --git a/public/fontiran.css b/public/fontiran.css
new file mode 100644
index 0000000..86d585d
--- /dev/null
+++ b/public/fontiran.css
@@ -0,0 +1,169 @@
+/**
+*
+* Name: IRAN Sans-Serif Font
+* Version: 5.0
+* Author: Moslem Ebrahimi (moslemebrahimi.com)
+* Created on: Dec 25, 2012
+* Updated on: Sep 01, 2017
+* Website: http://fontiran.com
+* Copyright: Commercial/Proprietary Software
+--------------------------------------------------------------------------------------
+فونت های ایران سن سریف یک نرم افزار مالکیتی محسوب می شود. جهت آگاهی از قوانین استفاده از این فونت ها لطفا به وب سایت (فونت ایران دات کام) مراجعه نمایید
+--------------------------------------------------------------------------------------
+IRAN Sans-serif fonts are considered a proprietary software. To gain information about the laws regarding the use of these fonts, please visit www.fontiran.com
+--------------------------------------------------------------------------------------
+This set of fonts are used in this project under the license: (.....)
+--------------------------------------------------------------------------------------
+*
+**/
+@font-face {
+ font-family: IRANSansFaNum;
+ font-style: normal;
+ font-weight: 900;
+ src: url('./fonts/eot/IRANSansWeb(FaNum)_Black.eot');
+ src: url('./fonts/eot/IRANSansWeb(FaNum)_Black.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
+ url('./fonts/woff2/IRANSansWeb(FaNum)_Black.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
+ url('./fonts/woff/IRANSansWeb(FaNum)_Black.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
+ url('./fonts/ttf/IRANSansWeb(FaNum)_Black.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: IRANSansFaNum;
+ font-style: normal;
+ font-weight: bold;
+ src: url('./fonts/eot/IRANSansWeb(FaNum)_Bold.eot');
+ src: url('./fonts/eot/IRANSansWeb(FaNum)_Bold.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
+ url('./fonts/woff2/IRANSansWeb(FaNum)_Bold.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
+ url('./fonts/woff/IRANSansWeb(FaNum)_Bold.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
+ url('./fonts/ttf/IRANSansWeb(FaNum)_Bold.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: IRANSansFaNum;
+ font-style: normal;
+ font-weight: 500;
+ src: url('./fonts/eot/IRANSansWeb(FaNum)_Medium.eot');
+ src: url('./fonts/eot/IRANSansWeb(FaNum)_Medium.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
+ url('./fonts/woff2/IRANSansWeb(FaNum)_Medium.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
+ url('./fonts/woff/IRANSansWeb(FaNum)_Medium.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
+ url('./fonts/ttf/IRANSansWeb(FaNum)_Medium.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: IRANSansFaNum;
+ font-style: normal;
+ font-weight: 300;
+ src: url('./fonts/eot/IRANSansWeb(FaNum)_Light.eot');
+ src: url('./fonts/eot/IRANSansWeb(FaNum)_Light.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
+ url('./fonts/woff2/IRANSansWeb(FaNum)_Light.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
+ url('./fonts/woff/IRANSansWeb(FaNum)_Light.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
+ url('./fonts/ttf/IRANSansWeb(FaNum)_Light.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: IRANSansFaNum;
+ font-style: normal;
+ font-weight: 200;
+ src: url('./fonts/eot/IRANSansWeb(FaNum)_UltraLight.eot');
+ src: url('./fonts/eot/IRANSansWeb(FaNum)_UltraLight.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
+ url('./fonts/woff2/IRANSansWeb(FaNum)_UltraLight.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
+ url('./fonts/woff/IRANSansWeb(FaNum)_UltraLight.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
+ url('./fonts/ttf/IRANSansWeb(FaNum)_UltraLight.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: IRANSansFaNum;
+ font-style: normal;
+ font-weight: normal;
+ src: url('./fonts/eot/IRANSansWeb(FaNum).eot');
+ src: url('./fonts/eot/IRANSansWeb(FaNum).eot?#iefix') format('embedded-opentype'), /* IE6-8 */
+ url('./fonts/woff2/IRANSansWeb(FaNum).woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
+ url('./fonts/woff/IRANSansWeb(FaNum).woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
+ url('./fonts/ttf/IRANSansWeb(FaNum).ttf') format('truetype');
+}
+
+@font-face {
+ font-family: IRANSans;
+ font-style: normal;
+ font-weight: 900;
+ src: url('./fonts/eot/IRANSansWeb_Black.eot');
+ src: url('./fonts/eot/IRANSansWeb_Black.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
+ url('./fonts/woff2/IRANSansWeb_Black.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
+ url('./fonts/woff/IRANSansWeb_Black.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
+ url('./fonts/ttf/IRANSansWeb_Black.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: IRANSans;
+ font-style: normal;
+ font-weight: bold;
+ src: url('./fonts/eot/IRANSansWeb_Bold.eot');
+ src: url('./fonts/eot/IRANSansWeb_Bold.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
+ url('./fonts/woff2/IRANSansWeb_Bold.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
+ url('./fonts/woff/IRANSansWeb_Bold.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
+ url('./fonts/ttf/IRANSansWeb_Bold.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: IRANSans;
+ font-style: normal;
+ font-weight: 500;
+ src: url('./fonts/eot/IRANSansWeb_Medium.eot');
+ src: url('./fonts/eot/IRANSansWeb_Medium.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
+ url('./fonts/woff2/IRANSansWeb_Medium.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
+ url('./fonts/woff/IRANSansWeb_Medium.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
+ url('./fonts/ttf/IRANSansWeb_Medium.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: IRANSans;
+ font-style: normal;
+ font-weight: 300;
+ src: url('./fonts/eot/IRANSansWeb_Light.eot');
+ src: url('./fonts/eot/IRANSansWeb_Light.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
+ url('./fonts/woff2/IRANSansWeb_Light.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
+ url('./fonts/woff/IRANSansWeb_Light.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
+ url('./fonts/ttf/IRANSansWeb_Light.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: IRANSans;
+ font-style: normal;
+ font-weight: 200;
+ src: url('./fonts/eot/IRANSansWeb_UltraLight.eot');
+ src: url('./fonts/eot/IRANSansWeb_UltraLight.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
+ url('./fonts/woff2/IRANSansWeb_UltraLight.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
+ url('./fonts/woff/IRANSansWeb_UltraLight.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
+ url('./fonts/ttf/IRANSansWeb_UltraLight.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: IRANSans;
+ font-style: normal;
+ font-weight: normal;
+ src: url('./fonts/eot/IRANSansWeb.eot');
+ src: url('./fonts/eot/IRANSansWeb.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
+ url('./fonts/woff2/IRANSansWeb.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
+ url('./fonts/woff/IRANSansWeb.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
+ url('./fonts/ttf/IRANSansWeb.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: Parastoo;
+ font-style: normal;
+ font-weight: normal;
+ src: url('./fonts/eot/Parastoo.eot');
+ src: url('./fonts/eot/Parastoo.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
+ url('./fonts/woff/Parastoo.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
+ url('./fonts/ttf/Parastoo.ttf') format('truetype');
+}
+
+@font-face {
+ font-family: Parastoo;
+ font-style: normal;
+ font-weight: bold;
+ src: url('./fonts/eot/Parastoo-Bold.eot');
+ src: url('./fonts/eot/Parastoo-Bold.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
+ url('./fonts/woff/Parastoo-Bold.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
+ url('./fonts/ttf/Parastoo-Bold.ttf') format('truetype');
+}
diff --git a/public/fonts/eot/IRANSansWeb(FaNum).eot b/public/fonts/eot/IRANSansWeb(FaNum).eot
new file mode 100644
index 0000000..5a6fdd0
Binary files /dev/null and b/public/fonts/eot/IRANSansWeb(FaNum).eot differ
diff --git a/public/fonts/eot/IRANSansWeb(FaNum)_Black.eot b/public/fonts/eot/IRANSansWeb(FaNum)_Black.eot
new file mode 100644
index 0000000..a012bc6
Binary files /dev/null and b/public/fonts/eot/IRANSansWeb(FaNum)_Black.eot differ
diff --git a/public/fonts/eot/IRANSansWeb(FaNum)_Bold.eot b/public/fonts/eot/IRANSansWeb(FaNum)_Bold.eot
new file mode 100644
index 0000000..82525e5
Binary files /dev/null and b/public/fonts/eot/IRANSansWeb(FaNum)_Bold.eot differ
diff --git a/public/fonts/eot/IRANSansWeb(FaNum)_Light.eot b/public/fonts/eot/IRANSansWeb(FaNum)_Light.eot
new file mode 100644
index 0000000..f899008
Binary files /dev/null and b/public/fonts/eot/IRANSansWeb(FaNum)_Light.eot differ
diff --git a/public/fonts/eot/IRANSansWeb(FaNum)_Medium.eot b/public/fonts/eot/IRANSansWeb(FaNum)_Medium.eot
new file mode 100644
index 0000000..117c2c8
Binary files /dev/null and b/public/fonts/eot/IRANSansWeb(FaNum)_Medium.eot differ
diff --git a/public/fonts/eot/IRANSansWeb(FaNum)_UltraLight.eot b/public/fonts/eot/IRANSansWeb(FaNum)_UltraLight.eot
new file mode 100644
index 0000000..9cd9b66
Binary files /dev/null and b/public/fonts/eot/IRANSansWeb(FaNum)_UltraLight.eot differ
diff --git a/public/fonts/eot/IRANSansWeb.eot b/public/fonts/eot/IRANSansWeb.eot
new file mode 100644
index 0000000..553eec8
Binary files /dev/null and b/public/fonts/eot/IRANSansWeb.eot differ
diff --git a/public/fonts/eot/IRANSansWeb_Black.eot b/public/fonts/eot/IRANSansWeb_Black.eot
new file mode 100644
index 0000000..ee7c6a0
Binary files /dev/null and b/public/fonts/eot/IRANSansWeb_Black.eot differ
diff --git a/public/fonts/eot/IRANSansWeb_Bold.eot b/public/fonts/eot/IRANSansWeb_Bold.eot
new file mode 100644
index 0000000..746a6e6
Binary files /dev/null and b/public/fonts/eot/IRANSansWeb_Bold.eot differ
diff --git a/public/fonts/eot/IRANSansWeb_Light.eot b/public/fonts/eot/IRANSansWeb_Light.eot
new file mode 100644
index 0000000..f79a681
Binary files /dev/null and b/public/fonts/eot/IRANSansWeb_Light.eot differ
diff --git a/public/fonts/eot/IRANSansWeb_Medium.eot b/public/fonts/eot/IRANSansWeb_Medium.eot
new file mode 100644
index 0000000..8b31d6b
Binary files /dev/null and b/public/fonts/eot/IRANSansWeb_Medium.eot differ
diff --git a/public/fonts/eot/IRANSansWeb_UltraLight.eot b/public/fonts/eot/IRANSansWeb_UltraLight.eot
new file mode 100644
index 0000000..fd4b205
Binary files /dev/null and b/public/fonts/eot/IRANSansWeb_UltraLight.eot differ
diff --git a/public/fonts/eot/Parastoo-Bold.eot b/public/fonts/eot/Parastoo-Bold.eot
new file mode 100644
index 0000000..4dc70ba
Binary files /dev/null and b/public/fonts/eot/Parastoo-Bold.eot differ
diff --git a/public/fonts/eot/Parastoo.eot b/public/fonts/eot/Parastoo.eot
new file mode 100644
index 0000000..fd15eda
Binary files /dev/null and b/public/fonts/eot/Parastoo.eot differ
diff --git a/public/fonts/ttf/IRANSansWeb(FaNum).ttf b/public/fonts/ttf/IRANSansWeb(FaNum).ttf
new file mode 100644
index 0000000..cd74265
Binary files /dev/null and b/public/fonts/ttf/IRANSansWeb(FaNum).ttf differ
diff --git a/public/fonts/ttf/IRANSansWeb(FaNum)_Black.ttf b/public/fonts/ttf/IRANSansWeb(FaNum)_Black.ttf
new file mode 100644
index 0000000..0f84dbf
Binary files /dev/null and b/public/fonts/ttf/IRANSansWeb(FaNum)_Black.ttf differ
diff --git a/public/fonts/ttf/IRANSansWeb(FaNum)_Bold.ttf b/public/fonts/ttf/IRANSansWeb(FaNum)_Bold.ttf
new file mode 100644
index 0000000..6fa2412
Binary files /dev/null and b/public/fonts/ttf/IRANSansWeb(FaNum)_Bold.ttf differ
diff --git a/public/fonts/ttf/IRANSansWeb(FaNum)_Light.ttf b/public/fonts/ttf/IRANSansWeb(FaNum)_Light.ttf
new file mode 100644
index 0000000..d9000c9
Binary files /dev/null and b/public/fonts/ttf/IRANSansWeb(FaNum)_Light.ttf differ
diff --git a/public/fonts/ttf/IRANSansWeb(FaNum)_Medium.ttf b/public/fonts/ttf/IRANSansWeb(FaNum)_Medium.ttf
new file mode 100644
index 0000000..e20001c
Binary files /dev/null and b/public/fonts/ttf/IRANSansWeb(FaNum)_Medium.ttf differ
diff --git a/public/fonts/ttf/IRANSansWeb(FaNum)_UltraLight.ttf b/public/fonts/ttf/IRANSansWeb(FaNum)_UltraLight.ttf
new file mode 100644
index 0000000..0072606
Binary files /dev/null and b/public/fonts/ttf/IRANSansWeb(FaNum)_UltraLight.ttf differ
diff --git a/public/fonts/ttf/IRANSansWeb.ttf b/public/fonts/ttf/IRANSansWeb.ttf
new file mode 100644
index 0000000..7416c53
Binary files /dev/null and b/public/fonts/ttf/IRANSansWeb.ttf differ
diff --git a/public/fonts/ttf/IRANSansWeb_Black.ttf b/public/fonts/ttf/IRANSansWeb_Black.ttf
new file mode 100644
index 0000000..df4928f
Binary files /dev/null and b/public/fonts/ttf/IRANSansWeb_Black.ttf differ
diff --git a/public/fonts/ttf/IRANSansWeb_Bold.ttf b/public/fonts/ttf/IRANSansWeb_Bold.ttf
new file mode 100644
index 0000000..158fbaa
Binary files /dev/null and b/public/fonts/ttf/IRANSansWeb_Bold.ttf differ
diff --git a/public/fonts/ttf/IRANSansWeb_Light.ttf b/public/fonts/ttf/IRANSansWeb_Light.ttf
new file mode 100644
index 0000000..3060c60
Binary files /dev/null and b/public/fonts/ttf/IRANSansWeb_Light.ttf differ
diff --git a/public/fonts/ttf/IRANSansWeb_Medium.ttf b/public/fonts/ttf/IRANSansWeb_Medium.ttf
new file mode 100644
index 0000000..6dd6c5d
Binary files /dev/null and b/public/fonts/ttf/IRANSansWeb_Medium.ttf differ
diff --git a/public/fonts/ttf/IRANSansWeb_UltraLight.ttf b/public/fonts/ttf/IRANSansWeb_UltraLight.ttf
new file mode 100644
index 0000000..7ebe1bf
Binary files /dev/null and b/public/fonts/ttf/IRANSansWeb_UltraLight.ttf differ
diff --git a/public/fonts/ttf/Parastoo-Bold.ttf b/public/fonts/ttf/Parastoo-Bold.ttf
new file mode 100644
index 0000000..5ef27c9
Binary files /dev/null and b/public/fonts/ttf/Parastoo-Bold.ttf differ
diff --git a/public/fonts/ttf/Parastoo.ttf b/public/fonts/ttf/Parastoo.ttf
new file mode 100644
index 0000000..f52889c
Binary files /dev/null and b/public/fonts/ttf/Parastoo.ttf differ
diff --git a/public/fonts/woff/IRANSansWeb(FaNum).woff b/public/fonts/woff/IRANSansWeb(FaNum).woff
new file mode 100644
index 0000000..e34dfc4
Binary files /dev/null and b/public/fonts/woff/IRANSansWeb(FaNum).woff differ
diff --git a/public/fonts/woff/IRANSansWeb(FaNum)_Black.woff b/public/fonts/woff/IRANSansWeb(FaNum)_Black.woff
new file mode 100644
index 0000000..bbbca77
Binary files /dev/null and b/public/fonts/woff/IRANSansWeb(FaNum)_Black.woff differ
diff --git a/public/fonts/woff/IRANSansWeb(FaNum)_Bold.woff b/public/fonts/woff/IRANSansWeb(FaNum)_Bold.woff
new file mode 100644
index 0000000..05d933e
Binary files /dev/null and b/public/fonts/woff/IRANSansWeb(FaNum)_Bold.woff differ
diff --git a/public/fonts/woff/IRANSansWeb(FaNum)_Light.woff b/public/fonts/woff/IRANSansWeb(FaNum)_Light.woff
new file mode 100644
index 0000000..fe80d47
Binary files /dev/null and b/public/fonts/woff/IRANSansWeb(FaNum)_Light.woff differ
diff --git a/public/fonts/woff/IRANSansWeb(FaNum)_Medium.woff b/public/fonts/woff/IRANSansWeb(FaNum)_Medium.woff
new file mode 100644
index 0000000..f13a870
Binary files /dev/null and b/public/fonts/woff/IRANSansWeb(FaNum)_Medium.woff differ
diff --git a/public/fonts/woff/IRANSansWeb(FaNum)_UltraLight.woff b/public/fonts/woff/IRANSansWeb(FaNum)_UltraLight.woff
new file mode 100644
index 0000000..9c653a4
Binary files /dev/null and b/public/fonts/woff/IRANSansWeb(FaNum)_UltraLight.woff differ
diff --git a/public/fonts/woff/IRANSansWeb.woff b/public/fonts/woff/IRANSansWeb.woff
new file mode 100644
index 0000000..abb63f2
Binary files /dev/null and b/public/fonts/woff/IRANSansWeb.woff differ
diff --git a/public/fonts/woff/IRANSansWeb_Black.woff b/public/fonts/woff/IRANSansWeb_Black.woff
new file mode 100644
index 0000000..12c8a6e
Binary files /dev/null and b/public/fonts/woff/IRANSansWeb_Black.woff differ
diff --git a/public/fonts/woff/IRANSansWeb_Bold.woff b/public/fonts/woff/IRANSansWeb_Bold.woff
new file mode 100644
index 0000000..801c55d
Binary files /dev/null and b/public/fonts/woff/IRANSansWeb_Bold.woff differ
diff --git a/public/fonts/woff/IRANSansWeb_Light.woff b/public/fonts/woff/IRANSansWeb_Light.woff
new file mode 100644
index 0000000..99b8d5d
Binary files /dev/null and b/public/fonts/woff/IRANSansWeb_Light.woff differ
diff --git a/public/fonts/woff/IRANSansWeb_Medium.woff b/public/fonts/woff/IRANSansWeb_Medium.woff
new file mode 100644
index 0000000..322c396
Binary files /dev/null and b/public/fonts/woff/IRANSansWeb_Medium.woff differ
diff --git a/public/fonts/woff/IRANSansWeb_UltraLight.woff b/public/fonts/woff/IRANSansWeb_UltraLight.woff
new file mode 100644
index 0000000..31c21ca
Binary files /dev/null and b/public/fonts/woff/IRANSansWeb_UltraLight.woff differ
diff --git a/public/fonts/woff/Parastoo-Bold.woff b/public/fonts/woff/Parastoo-Bold.woff
new file mode 100644
index 0000000..c6c05ff
Binary files /dev/null and b/public/fonts/woff/Parastoo-Bold.woff differ
diff --git a/public/fonts/woff/Parastoo.woff b/public/fonts/woff/Parastoo.woff
new file mode 100644
index 0000000..e5efc4d
Binary files /dev/null and b/public/fonts/woff/Parastoo.woff differ
diff --git a/public/fonts/woff2/IRANSansWeb(FaNum).woff2 b/public/fonts/woff2/IRANSansWeb(FaNum).woff2
new file mode 100644
index 0000000..30a377d
Binary files /dev/null and b/public/fonts/woff2/IRANSansWeb(FaNum).woff2 differ
diff --git a/public/fonts/woff2/IRANSansWeb(FaNum)_Black.woff2 b/public/fonts/woff2/IRANSansWeb(FaNum)_Black.woff2
new file mode 100644
index 0000000..0b15441
Binary files /dev/null and b/public/fonts/woff2/IRANSansWeb(FaNum)_Black.woff2 differ
diff --git a/public/fonts/woff2/IRANSansWeb(FaNum)_Bold.woff2 b/public/fonts/woff2/IRANSansWeb(FaNum)_Bold.woff2
new file mode 100644
index 0000000..f2353b2
Binary files /dev/null and b/public/fonts/woff2/IRANSansWeb(FaNum)_Bold.woff2 differ
diff --git a/public/fonts/woff2/IRANSansWeb(FaNum)_Light.woff2 b/public/fonts/woff2/IRANSansWeb(FaNum)_Light.woff2
new file mode 100644
index 0000000..562d663
Binary files /dev/null and b/public/fonts/woff2/IRANSansWeb(FaNum)_Light.woff2 differ
diff --git a/public/fonts/woff2/IRANSansWeb(FaNum)_Medium.woff2 b/public/fonts/woff2/IRANSansWeb(FaNum)_Medium.woff2
new file mode 100644
index 0000000..a69f4d0
Binary files /dev/null and b/public/fonts/woff2/IRANSansWeb(FaNum)_Medium.woff2 differ
diff --git a/public/fonts/woff2/IRANSansWeb(FaNum)_UltraLight.woff2 b/public/fonts/woff2/IRANSansWeb(FaNum)_UltraLight.woff2
new file mode 100644
index 0000000..1114f71
Binary files /dev/null and b/public/fonts/woff2/IRANSansWeb(FaNum)_UltraLight.woff2 differ
diff --git a/public/fonts/woff2/IRANSansWeb.woff2 b/public/fonts/woff2/IRANSansWeb.woff2
new file mode 100644
index 0000000..21bbba5
Binary files /dev/null and b/public/fonts/woff2/IRANSansWeb.woff2 differ
diff --git a/public/fonts/woff2/IRANSansWeb_Black.woff2 b/public/fonts/woff2/IRANSansWeb_Black.woff2
new file mode 100644
index 0000000..da34aad
Binary files /dev/null and b/public/fonts/woff2/IRANSansWeb_Black.woff2 differ
diff --git a/public/fonts/woff2/IRANSansWeb_Bold.woff2 b/public/fonts/woff2/IRANSansWeb_Bold.woff2
new file mode 100644
index 0000000..dd1436c
Binary files /dev/null and b/public/fonts/woff2/IRANSansWeb_Bold.woff2 differ
diff --git a/public/fonts/woff2/IRANSansWeb_Light.woff2 b/public/fonts/woff2/IRANSansWeb_Light.woff2
new file mode 100644
index 0000000..16dc9f1
Binary files /dev/null and b/public/fonts/woff2/IRANSansWeb_Light.woff2 differ
diff --git a/public/fonts/woff2/IRANSansWeb_Medium.woff2 b/public/fonts/woff2/IRANSansWeb_Medium.woff2
new file mode 100644
index 0000000..5fb39d4
Binary files /dev/null and b/public/fonts/woff2/IRANSansWeb_Medium.woff2 differ
diff --git a/public/fonts/woff2/IRANSansWeb_UltraLight.woff2 b/public/fonts/woff2/IRANSansWeb_UltraLight.woff2
new file mode 100644
index 0000000..eeeec11
Binary files /dev/null and b/public/fonts/woff2/IRANSansWeb_UltraLight.woff2 differ
diff --git a/public/icons/favicon.png b/public/icons/favicon.png
new file mode 100644
index 0000000..c82bf9a
Binary files /dev/null and b/public/icons/favicon.png differ
diff --git a/public/locales/fa/app.json b/public/locales/fa/app.json
new file mode 100644
index 0000000..04ffca2
--- /dev/null
+++ b/public/locales/fa/app.json
@@ -0,0 +1,272 @@
+{
+ "app_name": "سامانه CRM",
+ "app_short_name": "سامانه CRM",
+ "dashboard": "داشبورد",
+ "powered_by_witel": "توسعه یافته توسط وایتل",
+ "first_page": "خوش آمدید",
+ "login": "ورود",
+ "login_expert": "ورود کارشناس",
+ "expert": "کارشناس",
+ "last_updated_at": "اخرین بروز رسانی",
+ "filter_mode": "حالت فیلتر",
+ "date_picker_label": "فیلتر بر اساس تاریخ",
+ "lessThan": "کوچکتر از",
+ "greaterThan": "بزرگتر از",
+ "between": "میان",
+ "online_message": "شما به اینترنت وصل هستید",
+ "offline_message": "اتصال شما به اینترنت قطع شده است",
+ "socket_is_connect_message": "شما به سوکت وصل هستید",
+ "socket_is_not_connect_message": "اتصال شما به سوکت قطع شده است",
+ "header": {
+ "open_profile": "پروفایل",
+ "edit_profile": " پروفایل",
+ "change_password": "تغییر رمز عبور",
+ "logout": "خروج"
+ },
+ "Titles": {
+ "title_login_page": "صفحه ورود",
+ "title_login_expert_page": "ورود کارشناس",
+ "title_custom_404": "صفحه یافت نشد",
+ "title_custom_403": "عدم دسترسی",
+ "title_custom_500": "خطای سرور"
+ },
+ "sidebar": {
+ "dashboard": "داشبورد",
+ "change-password": "تغییر رمز عبور",
+ "edit-profile": "ویرایش پروفایل",
+ "role-management": "مدیریت نقش ها",
+ "admin": "مدیریت",
+ "expert-management": "مدیریت کارشناسان"
+ },
+ "secondary": {
+ },
+ "Authorization": {
+ "typography_your_login_is_valid_and_you_do_not_need_to_login_again": "شما دسترسی لازم به این صفحه را دارید نیاز به ورود مجدد نیست.",
+ "typography_your_access_to_this_page_has_expired_Please_login_again": "دسترسی شما منقضی شده است لطفا دوباره ورود نمایید.",
+ "typography_redirect_to": "درحال رفتن به صفحه",
+ "typography_routing_previuos_page": " قبل...",
+ "typography_routing_dashbaord_page": "داشبورد..."
+ },
+ "Permission": {
+ "typography_you_dont_have_access": "شما دسترسی لازم برای مشاهده این صفحه را ندارید!",
+ "button_back_dashboard": "بازگشت به صفحه اصلی"
+ },
+ "LoginPage": {
+ "text_field_user_name": "نام کاربری",
+ "text_field_enter_your_username": "نام کاربری خود را وارد کنید",
+ "text_field_password": "رمز عبور",
+ "text_field_enter_your_password": "رمز عبور خود را وارد کنید",
+ "button_submit": "ورود",
+ "link_routing_back_to": "بازگشت به",
+ "link_routing_previuos_page": "صفحه قبل",
+ "link_routing_main_page": "صفحه اصلی",
+ "username_error_message_required": "وارد کردن نام کاربری الزامیست",
+ "password_error_message_required": "وارد کردن رمز عبور الزامیست"
+ },
+ "ErrorPage": {
+ "custom_404": "صفحه مورد نظر یافت نشد | 404",
+ "custom_500": "دسترسی به سامانه امکانپذیر نمی باشد دقایقی دیگر تلاش نمائید | 500",
+ "custom_403": "شما دسترسی به این بخش را ندارید | 403",
+ "link_routing_back_to": "بازگشت به",
+ "link_routing_previuos_page": "صفحه قبل",
+ "link_routing_main_page": "صفحه اصلی"
+ },
+ "Dashboard": {
+ "dashboard_page": "داشبورد",
+ "change_password": "تغییر رمز عبور",
+ "edit_profile": "ویرایش پروفایل",
+ "role_management_page": "مدیریت نقش ها",
+ "expert_management": "مدیریت کارشناسان"
+ },
+ "MuiDatePicker": {
+ "date_picker_birthday": "تاریخ"
+ },
+ "ChangePassword": {
+ "typography_change_password": "تغییر رمز عبور",
+ "label_current_password": "رمز عبور فعلی",
+ "label_new_password": "رمز عبور جدید",
+ "label_confirm_password": "تکرار رمز عبور جدید",
+ "error_message_current_password_required": "وارد کردن رمز عبور فعلی الزامیست!",
+ "error_message_new_password_required": "وارد کردن رمز عبور جدید الزامیست!",
+ "error_message_confirm_password_required": "وارد کردن تکرار رمز عبور جدید الزامیست!",
+ "error_message_password_length": "رمز عبور باید حداقل 8 کاراکتر باشد.",
+ "error_message_password_match": "پسورد ها یکسان هستند",
+ "error_message_password_not_match": "پسورد و تکرار رمز عبور باید یکسان باشد"
+ },
+ "SubmitButton": {
+ "button_while_submit": "در حال ثبت",
+ "button_submit": "ثبت"
+ },
+ "UpdateProfile": {
+ "error_invalid_email": "ایمیل نامعتبر است",
+ "typography_edit_profile": "پروفایل",
+ "text_field_email": "ایمیل",
+ "text_field_enter_your_password": "رمز عبور خود را وارد کنید",
+ "text_field_username": "نام کاربری",
+ "text_field_name": "نام",
+ "text_field_phone_number": "شماره تماس",
+ "text_field_position": "سمت",
+ "text_field_province_name": "نام استان",
+ "error_message_required": "اجباری !"
+ },
+ "notifications": {
+ "code": "کد",
+ "error": "خطا",
+ "warning": "خطا",
+ "success": "موفق",
+ "error_static_text": "عملیات شما با خطا مواجه شد",
+ "warning_static_text": "خطا سرور",
+ "success_static_text": "عملیات شما با موفقیت انجام شد",
+ "pending": "در حال انجام..."
+ },
+ "UploadSystem": {
+ "upload_file": "فایل خود را بارگذاری کنید",
+ "optional": " (اختیاری)",
+ "upload_file_unit": "حداکثر 2Mb",
+ "upload_file_format": "فرمت قابل قبول : png,jpg,pdf",
+ "delete": "پاک کردن",
+ "uploadfile_error": "حجم فایل بیشتر از 2 مگابایت می باشد"
+ },
+ "RoleManagement": {
+ "id": "کد یکتا",
+ "name_fa": "نام فارسی ",
+ "name": "نام انگلیسی",
+ "created_at": "تاریخ درخواست",
+ "updated_at": "تاریخ بروزرسانی"
+ },
+ "AddDialog": {
+ "add": "افزودن",
+ "name_en": "نام انگلیسی",
+ "name_en_error": "وارد کردن نام انگلیسی الزامیست",
+ "name_fa": "نام فارسی",
+ "name_fa_error": "وارد کردن نام فارسی الزامیست",
+ "permission": "دسترسی",
+ "permission_min_error": "حداقل باید یک دسترسی انتخاب شود",
+ "phone_number": "شماره تلفن",
+ "phone_number_positive": "شماره تلفن باید مثبت باشد",
+ "phone_number_error": "وارد کردن شماره تلفن الزامیست",
+ "phone_number_number": "شماره تلفن باید شامل اعداد باشد",
+ "phone_number_max": "شماره تلفن باید شامل 11 رقم باشد",
+ "national_id_positive": "کد ملی باید مثبت باشد",
+ "national_id_number": "کد ملی باید شامل اعداد باشد",
+ "national_id_max": "کد ملی باید شامل 10 رقم باشد",
+ "national_id": "کد ملی",
+ "national_id_error": "وارد کردن کد ملی الزامیست",
+ "type_id_error": "وارد کردن نوع کاربر الزامیست",
+ "navgan_id_error": "وارد کردن کد ناوگان الزامیست",
+ "refahi": "رفاهی",
+ "navgan": "ناوگان",
+ "type_id": "نوع کاربر",
+ "navgan_id": "کد ناوگان",
+ "button-cancel": "انصراف",
+ "loading_permissions_list": "درحال دریافت لیست دسترسی ها",
+ "button-add": "ثبت"
+ },
+ "UpdateDialog": {
+ "update": "ویرایش",
+ "description": "توضیحات خود را وارد نمائید",
+ "description_error": "وارد کردن توضیحات الزامی است!",
+ "next-state-id": "وضعیت",
+ "next-state-id-error": "وارد کردن وضعیت الزامیست",
+ "update-tooltip": "ویرایش",
+ "button-cancel": "بستن",
+ "refahi": "رفاهی",
+ "navgan": "ناوگان",
+ "phone_number": "شماره تلفن",
+ "national_id": "کد ملی",
+ "name_en": "نام انگلیسی",
+ "name_en_error": "وارد کردن نام انگلیسی الزامیست",
+ "name_fa": "نام فارسی",
+ "name_fa_error": "وارد کردن نام فارسی الزامیست",
+ "permission": "دسترسی",
+ "permission_min_error": "حداقل باید یک دسترسی انتخاب شود",
+ "type_id": "نوع کاربر",
+ "navgan_id": "کد ناوگان",
+ "loading_permissions_list": "درحال دریافت لیست دسترسی ها",
+ "button-update": "ثبت"
+ },
+ "DeleteDialog": {
+ "delete": "حذف",
+ "button-cancel": "انصراف",
+ "typography": "آیا از حدف این مورد اطمینان دارید ؟",
+ "button-delete": "حذف کردن"
+ },
+ "ExpertMangement": {
+ "id": "کد یکتا",
+ "name": "نام کامل",
+ "username": "نام کاربری",
+ "email": "پست الکترونیک",
+ "phone_number": "شماره همراه",
+ "national_id": "کد ملی",
+ "position": "سمت",
+ "province_fa": "استان",
+ "role_name": "نقش",
+ "gender": "جنسیت",
+ "male": "مرد",
+ "female": "زن",
+ "updated_at": "آخرین بروزرسانی",
+ "create": "افزودن کارشناس",
+ "personal_info": "مشخصات کارشناس",
+ "user_info": "اطلاعات کاربری",
+ "rest_info": "اطلاعات تکمیلی",
+ "text_field_full_name": "نام کامل",
+ "text_field_username": "نام کاربری",
+ "text_field_email": "پست الکترونیک",
+ "text_field_phone_number": "شماره همراه",
+ "text_field_telephone_id": "کد تلفن",
+ "text_field_gender": "جنسیت",
+ "text_field_national_id": "کد ملی",
+ "text_field_password": "رمز عبور",
+ "text_field_position": "سمت",
+ "text_field_province_id": "استان",
+ "text_field_roles": "نقش",
+ "text_field_loading_provinces_list": "درحال دریافت شهر ها",
+ "text_field_error_fetching_provinces": "خطا در دریافت شهر ها",
+ "text_field_loading_roles_list": "درحال دریافت نقش ها",
+ "text_field_error_fetching_roles": "خطا در دریافت نقش ها",
+ "text_field_please_select_province": "ابتدا استان خود را انتخاب کنید",
+ "text_field_loading_cities_list": "درحال دریافت شهر ها",
+ "text_field_error_fetching_cities": "خطا در دریافت شهر ها",
+ "text_field_new_password": "رمز عبور جدید",
+ "error_message_full_name": "نام کامل خود را وارد کنید",
+ "error_message_username": "نام کاربری خود را وارد کنید",
+ "error_message_email": "پست الکترونیک خود را وارد کنید",
+ "error_message_phone_number": "شماره همراه خود را وارد کنید",
+ "error_message_telephone_id": "کد تلفن خود را وارد کنید",
+ "error_message_gender": "جنسیت خود را وارد کنید",
+ "error_message_national_id": "کد ملی خود را وارد کنید",
+ "error_message_password": "رمز عبور خود را وارد کنید",
+ "error_message_password_regex": "رمز عبور باید حداقل 8 کاراکتر و متشکل از عدد، حرف یا سمبل باشد",
+ "error_message_position": "سمت خود را وارد کنید",
+ "error_message_province_id": "استان خود را وارد کنید",
+ "error_message_roles": "نقش خود را وارد کنید",
+ "error_message_new_password": "رمز عبور جدید را وارد کنید",
+ "error_message_new_password_regex": "رمز عبور باید حداقل 8 کاراکتر و متشکل از عدد، حرف یا سمبل باشد",
+ "button_cancel": "بستن",
+ "button_confirm": "ثبت",
+ "delete_expert": "حذف کارشناس",
+ "are_you_sure_text": "آیا از انجام این عملیات اطمینان دارید؟",
+ "button_delete": "حذف",
+ "delete_tooltip": "حذف",
+ "update_tooltip": "ویرایش",
+ "update_expert": "ویرایش کارشناس"
+ },
+ "CallHistory": {
+ "category_name": "موضوع",
+ "subcategory_name": "زیر موضوع",
+ "operator": "اپراتور",
+ "date": "تاریخ",
+ "call_history_of": "تاریخچه تماس های",
+ "call_history_not_found": "تاریخچه ای برای تماس دریافتی یافت نشد",
+ "call_history_error_fetching": "خطا در دریافت تاریخچه تماس دریافتی",
+ "show_more": "نمایش موارد بیشتر"
+ },
+ "CallAction": {
+ "call_history_of": "عملیات های مربوط به تماس",
+ "categories" : "موضوع ها",
+ "subcategories" : "زیر موضوع ها",
+ "description" : "توضیحات تماس (اختیاری)",
+ "text_field_label" : "توضیحات",
+ "text_field_palacholder" : "لطفا توضیحات خود را وارد نمایید"
+ }
+}
diff --git a/public/locales/fa/datatable.js b/public/locales/fa/datatable.js
new file mode 100644
index 0000000..fd43663
--- /dev/null
+++ b/public/locales/fa/datatable.js
@@ -0,0 +1,92 @@
+export const FA_DATATABLE_LOCALIZATION = {
+ actions: "عملیات",
+ and: "و",
+ cancel: "لغو",
+ changeFilterMode: "تغییر حالت فیلتر",
+ changeSearchMode: "تغییر حالت جستجو",
+ clearFilter: "پاک کردن فیلتر",
+ clearSearch: "پاک کردن جستجو",
+ clearSort: "پاک کردن مرتب سازی",
+ clickToCopy: "کلیک برای کپی",
+ collapse: "جمع شدن",
+ collapseAll: "جمع شدن همه",
+ columnActions: "عملیات ستون",
+ copiedToClipboard: "کپی شد",
+ dropToGroupBy: "رها کردن برای گروه بندی بر اساس {column}",
+ edit: "ویرایش",
+ expand: "باز شدن",
+ expandAll: "باز شدن همه",
+ filterArrIncludes: "شامل",
+ filterArrIncludesAll: "شامل همه",
+ filterArrIncludesSome: "شامل",
+ filterBetween: "میان",
+ filterBetweenInclusive: "میان با احتساب هر دو",
+ filterByColumn: "فیلتر بر اساس {column}",
+ filterContains: "شامل",
+ filterEmpty: "خالی",
+ filterEndsWith: "به پایان میرسد با",
+ filterEquals: "برابر",
+ filterEqualsString: "برابر",
+ filterFuzzy: "نزدیک",
+ filterGreaterThan: "بزرگتر از",
+ filterGreaterThanOrEqualTo: "بزرگتر یا مساوی",
+ filterInNumberRange: "میان",
+ filterIncludesString: "شامل",
+ filterIncludesStringSensitive: "شامل",
+ filterLessThan: "کوچکتر از",
+ filterLessThanOrEqualTo: "کوچکتر یا مساوی",
+ filterMode: "حالت فیلتر: {filterType}",
+ filterNotEmpty: "غیر خالی",
+ filterNotEquals: "نا برابر",
+ filterStartsWith: "شروع میشود با",
+ filterWeakEquals: "برابر",
+ filteringByColumn: "فیلتر بر اساس {column} - {filterType} {filterValue}",
+ goToFirstPage: "رفتن به صفحه اول",
+ goToLastPage: "رفتن به صفحه آخر",
+ goToNextPage: "رفتن به صفحه بعدی",
+ goToPreviousPage: "رفتن به صفحه قبلی",
+ grab: "گرفتن",
+ groupByColumn: "گروه بندی بر اساس {column}",
+ groupedBy: "گروه بندی شده بر اساس",
+ hideAll: "پنهان کردن همه",
+ hideColumn: "پنهان کردن ستون {column}",
+ max: "حداکثر",
+ min: "حداقل",
+ move: "انتقال",
+ noRecordsToDisplay: "هیچ رکوردی برای نمایش وجود ندارد",
+ noResultsFound: "نتیجهای یافت نشد",
+ of: "از",
+ or: "یا",
+ pinToLeft: "چسباندن به سمت چپ",
+ pinToRight: "چسباندن به سمت راست",
+ resetColumnSize: "بازنشانی اندازه ستون",
+ resetOrder: "بازنشانی ترتیب",
+ rowActions: "عملیات ردیف",
+ rowNumber: "#",
+ rowNumbers: "شماره ردیف",
+ rowsPerPage: "ردیف در هر صفحه",
+ save: "ذخیره",
+ search: "جستجو",
+ selectedCountOfRowCountRowsSelected:
+ "{selectedCount} از {rowCount} ردیف انتخاب شده",
+ select: "انتخاب",
+ showAll: "نمایش همه",
+ showAllColumns: "نمایش همه ستونها",
+ showHideColumns: "نمایش/مخفی کردن ستونها",
+ showHideFilters: "نمایش/مخفی کردن فیلترها",
+ showHideSearch: "نمایش/مخفی کردن جستجو",
+ sortByColumnAsc: "مرتب سازی بر اساس {column} صعودی",
+ sortByColumnDesc: "مرتب سازی بر اساس {column} نزولی",
+ sortedByColumnAsc: "مرتب شده بر اساس {column} صعودی",
+ sortedByColumnDesc: "مرتب شده بر اساس {column} نزولی",
+ thenBy: "، سپس بر اساس ",
+ toggleDensity: "تغییر تراکم",
+ toggleFullScreen: "تغییر حالت تمام صفحه",
+ toggleSelectAll: "انتخاب/عدم انتخاب همه",
+ toggleSelectRow: "انتخاب/عدم انتخاب ردیف",
+ toggleVisibility: "تغییر پیدا/پنهان",
+ ungroupByColumn: "لغو گروه بندی بر اساس {column}",
+ unpin: "رها کردن",
+ unpinAll: "رها کردن همه",
+ unsorted: "بدون مرتب سازی",
+};
diff --git a/public/manifest.json b/public/manifest.json
new file mode 100644
index 0000000..9246992
--- /dev/null
+++ b/public/manifest.json
@@ -0,0 +1,57 @@
+{
+ "short_name": "CRM Dashboard",
+ "name": "CRM Expert Dashboard",
+ "description": "CRM",
+ "version": "1.0.0",
+ "version_name": "1.0.0 beta",
+ "start_url": "/dashboard",
+ "orientation": "portrait",
+ "display": "standalone",
+ "theme_color": "#084070",
+ "background_color": "#084070",
+ "icons": [
+ {
+ "src": "/icons/maskable_icon.png",
+ "sizes": "any",
+ "type": "image/png"
+ },
+ {
+ "src": "/icons/maskable_icon_x48.png",
+ "sizes": "48x48",
+ "type": "image/png"
+ },
+ {
+ "src": "/icons/maskable_icon_x72.png",
+ "sizes": "72x72",
+ "type": "image/png"
+ },
+ {
+ "src": "/icons/maskable_icon_x96.png",
+ "sizes": "96x96",
+ "type": "image/png"
+ },
+ {
+ "src": "/icons/maskable_icon_x128.png",
+ "sizes": "128x128",
+ "type": "image/png"
+ },
+ {
+ "src": "/icons/maskable_icon_x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/icons/maskable_icon_x384.png",
+ "sizes": "384x384",
+ "type": "image/png"
+ },
+ {
+ "src": "/icons/maskable_icon_x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "categories": [
+ "government"
+ ]
+}
diff --git a/src/components/dashboard/change-password/change-password-form/__test__/index.test.js b/src/components/dashboard/change-password/change-password-form/__test__/index.test.js
new file mode 100644
index 0000000..503b571
--- /dev/null
+++ b/src/components/dashboard/change-password/change-password-form/__test__/index.test.js
@@ -0,0 +1,305 @@
+import { render, screen, waitFor, fireEvent , act } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import MockAppWithProviders from "../../../../../../mocks/AppWithProvider";
+import ChangePasswordForm from "@/components/dashboard/change-password/change-password-form";
+import {SET_USER_PASSWORD} from "@/core/data/apiRoutes";
+import {server} from "../../../../../../mocks/server";
+import {rest} from "msw";
+
+describe('Change Password Form Component From Change Password Page', () => {
+ describe('Rendering', () => {
+ it('Should see change password heading',() => {
+ render();
+ const changePasswordElement = screen.getByRole('heading', { level: 4 });
+ expect(changePasswordElement).toHaveTextContent('تغییر رمز عبور');
+ });
+ it('Should see the label for current password field', () => {
+ render();
+ const currentPasswordLabel = screen.queryByLabelText('رمز عبور فعلی');
+ expect(currentPasswordLabel).toBeInTheDocument();
+ });
+ it('Should see the label for new password field', () => {
+ render();
+ const newPasswordLabel = screen.queryByLabelText('رمز عبور جدید');
+ expect(newPasswordLabel).toBeInTheDocument();
+ });
+ it('Should see the label for confirm password field', () => {
+ render();
+ const confirmPasswordLabel = screen.queryByLabelText('تکرار رمز عبور جدید');
+ expect(confirmPasswordLabel).toBeInTheDocument();
+ });
+ it('Should see the submit button with the submit label and should be disable', () => {
+ render();
+ const submitButton = screen.getByRole('button', { name: 'ثبت' });
+ expect(submitButton).toBeInTheDocument();
+ expect(submitButton).toBeDisabled();
+ });
+ it('Should have empty input fields and not see any validation error for current password, new password, and confirm password', () => {
+ render();
+ const currentPasswordInput = screen.queryByLabelText('رمز عبور فعلی');
+ const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
+ const confirmPasswordInput = screen.queryByLabelText('تکرار رمز عبور جدید');
+
+ expect(currentPasswordInput).toHaveValue('');
+ expect(newPasswordInput).toHaveValue('');
+ expect(confirmPasswordInput).toHaveValue('');
+
+ expect(screen.queryByText("وارد کردن رمز عبور فعلی الزامیست!")).not.toBeInTheDocument();
+ expect(screen.queryByText("وارد کردن رمز عبور جدید الزامیست!")).not.toBeInTheDocument();
+ expect(screen.queryByText("وارد کردن تکرار رمز عبور جدید الزامیست!")).not.toBeInTheDocument();
+
+ });
+ });
+ describe("Behavior", ()=>{
+ it('Should fill the current password field and not see any error while its not empty', async () => {
+ render();
+
+ const currentPasswordInput = screen.queryByLabelText('رمز عبور فعلی');
+
+ // Simulate typing something in the current password input
+ fireEvent.change(currentPasswordInput, { target: { value: 'witel@fani0' } });
+
+ await waitFor(() => {
+ expect(screen.queryByText("وارد کردن رمز عبور فعلی الزامیست!")).not.toBeInTheDocument();
+ expect(currentPasswordInput).toHaveValue('witel@fani0');
+ });
+ });
+ it('Should fill the new password field and not see any error while its not empty', async () => {
+ render();
+
+ const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
+
+ // Simulate typing something in the new password input
+ fireEvent.change(newPasswordInput, { target: { value: 'witel@fani1' } });
+
+ await waitFor(() => {
+ expect(screen.queryByText("وارد کردن رمز عبور جدید الزامیست!")).not.toBeInTheDocument();
+ expect(newPasswordInput).toHaveValue('witel@fani1');
+ });
+ });
+ it('Should fill the confirm new password field and not see any error while its not empty', async () => {
+ render();
+
+ const confirmPasswordInput = screen.queryByLabelText('تکرار رمز عبور جدید');
+
+ // Simulate typing something in the confirmation new password input
+ fireEvent.change(confirmPasswordInput, { target: { value: 'witel@fani1' } });
+
+ await waitFor(() => {
+ expect(screen.queryByText("وارد کردن تکرار رمز عبور جدید الزامیست!")).not.toBeInTheDocument();
+ expect(confirmPasswordInput).toHaveValue('witel@fani1');
+ });
+ });
+ });
+ describe("Validation", ()=>{
+ it('Should see error while current password is empty on blur event', async () => {
+ render();
+
+ // Simulate blur event on the current password input
+ const currentPasswordInput = screen.queryByLabelText('رمز عبور فعلی');
+
+ // Clear the input field
+ fireEvent.change(currentPasswordInput, { target: { value: '' } });
+
+ fireEvent.blur(currentPasswordInput);
+
+ await waitFor(()=>{
+ expect(screen.queryByText("وارد کردن رمز عبور فعلی الزامیست!")).toBeInTheDocument()
+ });
+ });
+ it('Should see error while new password is empty on blur event', async () => {
+ render();
+
+ // Simulate blur event on the new password input
+ const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
+
+ // Clear the input field
+ fireEvent.change(newPasswordInput, { target: { value: '' } });
+
+ fireEvent.blur(newPasswordInput);
+
+ await waitFor(()=>{
+ expect(screen.queryByText("وارد کردن رمز عبور جدید الزامیست!")).toBeInTheDocument()
+ });
+ });
+ it('Should see error while confirm new password is empty on blur event', async () => {
+ render();
+
+ // Simulate blur event on the new password input
+ const confirmPasswordInput = screen.queryByLabelText('تکرار رمز عبور جدید');
+
+ // Clear the input field
+ fireEvent.change(confirmPasswordInput, { target: { value: '' } });
+
+ fireEvent.blur(confirmPasswordInput);
+
+ await waitFor(()=>{
+ expect(screen.queryByText("وارد کردن تکرار رمز عبور جدید الزامیست!")).toBeInTheDocument()
+ });
+ });
+ it('Should see error when new password is less than 8 characters', async () => {
+ render();
+
+ const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
+ const submitButton = screen.getByRole('button', { name: 'ثبت' });
+
+ // Simulate entering a short new password
+ fireEvent.change(newPasswordInput, { target: { value: 'test' } });
+ fireEvent.blur(newPasswordInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("رمز عبور باید حداقل 8 کاراکتر باشد.")).toBeInTheDocument();
+ });
+
+ // Simulate clicking the submit button
+ fireEvent.click(submitButton);
+
+ await waitFor(() => {
+ expect(screen.queryByText("رمز عبور باید حداقل 8 کاراکتر باشد.")).toBeInTheDocument();
+ });
+ });
+ it('Should not see error when new password are 8 or more characters', async () => {
+ render();
+
+ const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
+ const submitButton = screen.getByRole('button', { name: 'ثبت' });
+
+ // Simulate entering a valid new password
+ fireEvent.change(newPasswordInput, { target: { value: 'Witel@fani1' } });
+ fireEvent.blur(newPasswordInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("رمز عبور باید حداقل 8 کاراکتر باشد.")).not.toBeInTheDocument();
+ });
+
+ // Simulate clicking the submit button
+ fireEvent.click(submitButton);
+
+ await waitFor(() => {
+ expect(screen.queryByText("رمز عبور باید حداقل 8 کاراکتر باشد.")).not.toBeInTheDocument();
+ });
+ });
+ it('Should see error when new password and confirm password do not match on blur event and submit click', async () => {
+ render();
+
+ const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
+ const confirmPasswordInput = screen.queryByLabelText('تکرار رمز عبور جدید');
+ const submitButton = screen.getByRole('button', { name: 'ثبت' });
+
+ // Simulate entering a valid new password
+ fireEvent.change(newPasswordInput, { target: { value: 'Witel@fani1' } });
+ fireEvent.blur(newPasswordInput);
+
+ // Simulate entering a different value in the confirm password field
+ fireEvent.change(confirmPasswordInput, { target: { value: 'MismatchedPassword' } });
+ fireEvent.blur(confirmPasswordInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("پسورد و تکرار رمز عبور باید یکسان باشد")).toBeInTheDocument();
+ });
+
+ // Simulate clicking the submit button
+ fireEvent.click(submitButton);
+
+ await waitFor(() => {
+ expect(screen.queryByText("پسورد و تکرار رمز عبور باید یکسان باشد")).toBeInTheDocument();
+ });
+ });
+ it('Should not see error when new password and confirm password match on blur event and submit click', async () => {
+ render();
+
+ const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
+ const confirmPasswordInput = screen.queryByLabelText('تکرار رمز عبور جدید');
+ const submitButton = screen.getByRole('button', { name: 'ثبت' });
+
+ // Simulate entering a valid new password
+ fireEvent.change(newPasswordInput, { target: { value: 'Witel@fani1' } });
+ fireEvent.blur(newPasswordInput);
+
+ // Simulate entering the same value in the confirmation password field
+ fireEvent.change(confirmPasswordInput, { target: { value: 'Witel@fani1' } });
+ fireEvent.blur(confirmPasswordInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("پسورد و تکرار رمز عبور باید یکسان باشد")).not.toBeInTheDocument();
+ });
+
+ // Simulate clicking the submit button
+ fireEvent.click(submitButton);
+
+ await waitFor(() => {
+ expect(screen.queryByText("پسورد و تکرار رمز عبور باید یکسان باشد")).not.toBeInTheDocument();
+ });
+ });
+ });
+ describe("Form Submission", () => {
+ it('Should enable the submit button when the inputs are valid', async () => {
+ render();
+
+ const submitButton = screen.getByRole('button', { name: 'ثبت' });
+
+ // Simulate entering valid values in the form fields
+ const currentPasswordInput = screen.queryByLabelText('رمز عبور فعلی');
+ const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
+ const confirmPasswordInput = screen.queryByLabelText('تکرار رمز عبور جدید');
+
+ fireEvent.change(currentPasswordInput, { target: { value: 'Witel@fani0' } });
+ fireEvent.change(newPasswordInput, { target: { value: 'Witel@fani1' } });
+ fireEvent.change(confirmPasswordInput, { target: { value: 'Witel@fani1' } });
+
+ // Check if the submit button is enabled
+ await waitFor(() => {
+ expect(submitButton).not.toBeDisabled();
+ });
+ });
+ it('Should request to api and resetform in success response', async () => {
+ // Render the component
+ render();
+
+ // Fill in the form fields
+ const currentPasswordInput = screen.queryByLabelText('رمز عبور فعلی');
+ const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
+ const confirmPasswordInput = screen.queryByLabelText('تکرار رمز عبور جدید');
+
+ fireEvent.change(currentPasswordInput, { target: { value: 'Witel@fani0' } });
+ fireEvent.change(newPasswordInput, { target: { value: 'Witel@fani1' } });
+ fireEvent.change(confirmPasswordInput, { target: { value: 'Witel@fani1' } });
+
+ // Submit the form
+ const submitButton = screen.getByRole('button', { name: 'ثبت' });
+ await userEvent.click(submitButton);
+
+ await waitFor(() => {
+ expect(currentPasswordInput).toHaveValue('');
+ expect(newPasswordInput).toHaveValue('');
+ expect(confirmPasswordInput).toHaveValue('');
+ });
+ });
+ it('Should request to api and not resetform in error response', async () => {
+ // Render the component
+ render();
+ server.use([rest.get(SET_USER_PASSWORD, (req, res, ctx) => {
+ return res(ctx.status(422))
+ })])
+
+ // Fill in the form fields wrong
+ const currentPasswordInput = screen.queryByLabelText('رمز عبور فعلی');
+ const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
+ const confirmPasswordInput = screen.queryByLabelText('تکرار رمز عبور جدید');
+
+ fireEvent.change(currentPasswordInput, { target: { value: 'incorrect' } });
+ fireEvent.change(newPasswordInput, { target: { value: 'Witel@fani1' } });
+ fireEvent.change(confirmPasswordInput, { target: { value: 'Witel@fani1' } });
+
+ // Submit the form
+ const submitButton = screen.getByRole('button', { name: 'ثبت' });
+ await userEvent.click(submitButton);
+
+ await waitFor(() => {
+ expect(currentPasswordInput.value).toBe('incorrect');
+ expect(newPasswordInput.value).toBe('Witel@fani1');
+ expect(confirmPasswordInput.value).toBe('Witel@fani1');
+ });
+ });
+ });
+})
\ No newline at end of file
diff --git a/src/components/dashboard/change-password/change-password-form/index.js b/src/components/dashboard/change-password/change-password-form/index.js
new file mode 100644
index 0000000..9b7704a
--- /dev/null
+++ b/src/components/dashboard/change-password/change-password-form/index.js
@@ -0,0 +1,137 @@
+import {Formik, Form} from "formik";
+import * as Yup from "yup";
+import {useTranslations} from "next-intl";
+import {Button, Paper, Stack, Typography} from "@mui/material";
+import PasswordField from "@/core/components/PasswordField";
+import StyledForm from "@/core/components/StyledForm";
+import useRequest from "@/lib/app/hooks/useRequest";
+import {SET_USER_PASSWORD} from "@/core/data/apiRoutes";
+
+const ChangePasswordForm = ({onSubmit}) => {
+ const t = useTranslations();
+ const requestServer = useRequest({auth: true})
+
+ const handleSubmit = (values, {setSubmitting, resetForm}) => {
+ requestServer(SET_USER_PASSWORD, 'post', {
+ data: {
+ current_password: values.current_password,
+ new_password: values.new_password,
+ new_password_confirmation: values.new_password_confirmation,
+ },
+ }).then((response) => {
+ resetForm();
+ }).catch(() => {
+ }).finally(() => {
+ setSubmitting(false);
+ });
+ };
+ const initialValues = {
+ current_password: "",
+ new_password: "",
+ new_password_confirmation: "",
+ };
+ const validationSchema = Yup.object().shape({
+ current_password: Yup.string().required(
+ t("ChangePassword.error_message_current_password_required")
+ ),
+ new_password: Yup.string()
+ .min(8, t("ChangePassword.error_message_password_length"))
+ .required(t("ChangePassword.error_message_new_password_required")),
+ new_password_confirmation: Yup.string()
+ .required(t("ChangePassword.error_message_confirm_password_required"))
+ .test(
+ t("ChangePassword.error_message_password_match"),
+ t("ChangePassword.error_message_password_not_match"), // Use the correct error message here
+ function (value) {
+ return this.parent.new_password === value;
+ }
+ ),
+ });
+
+ return (
+
+ {(props) => (
+ {
+ e.preventDefault();
+ props.handleSubmit();
+ }}
+ >
+
+
+
+ {t("ChangePassword.typography_change_password")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default ChangePasswordForm;
diff --git a/src/components/dashboard/change-password/index.jsx b/src/components/dashboard/change-password/index.jsx
new file mode 100644
index 0000000..74896a6
--- /dev/null
+++ b/src/components/dashboard/change-password/index.jsx
@@ -0,0 +1,18 @@
+import CenterLayout from "@/layouts/CenterLayout";
+import DashboardLayout from "@/layouts/DashboardLayout";
+import {Container} from "@mui/material";
+import ChangePasswordForm from "@/components/dashboard/change-password/change-password-form";
+
+const DashboardChangePasswordComponent = () => {
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+export default DashboardChangePasswordComponent;
diff --git a/src/components/dashboard/edit-profile/index.jsx b/src/components/dashboard/edit-profile/index.jsx
new file mode 100644
index 0000000..e35e3bf
--- /dev/null
+++ b/src/components/dashboard/edit-profile/index.jsx
@@ -0,0 +1,223 @@
+import StyledForm from "@/core/components/StyledForm";
+import CenterLayout from "@/layouts/CenterLayout";
+import DashboardLayout from "@/layouts/DashboardLayout";
+import useUser from "@/lib/app/hooks/useUser";
+import {Box, Container, Grid, Paper, Stack, TextField, Typography,} from "@mui/material";
+import * as Yup from "yup";
+import {Field, Formik} from "formik";
+import {useTranslations} from "next-intl";
+import AvatarUpload from "@/core/components/AvatarUpload";
+import axios from "axios";
+import {UPDATE_AVATAR} from "@/core/data/apiRoutes";
+import useDirection from "@/lib/app/hooks/useDirection";
+import Notifications from "@/core/components/notifications";
+import ImageResizer from "@/core/components/ImageConvertor";
+
+const DashboardEditProfile = () => {
+ const t = useTranslations();
+ const {user, token, getUser, changeUser} = useUser();
+ const {directionApp} = useDirection();
+
+ const editAvatar = async (avatar) => {
+ Notifications(t);
+ try {
+ const formData = new FormData();
+
+ if (avatar != null) {
+ var resizedAvatar;
+ resizedAvatar = await ImageResizer(avatar);
+ formData.append("avatar", resizedAvatar);
+ }
+
+ await axios.post(UPDATE_AVATAR, formData, {
+ headers: {
+ authorization: `Bearer ${token}`,
+ "Content-Type": "multipart/form-data",
+ },
+ });
+ } catch (error) {
+ Notifications(t, error.response);
+ throw error;
+ }
+ };
+ const handleSubmit = (values, {setSubmitting}) => {
+
+ };
+ const initialValues = {
+ expert_avatar: null,
+ username: user.username,
+ name: user.name,
+ email: user.email,
+ province_name: user.province_name,
+ position: user.position,
+ };
+ const validationSchema = Yup.object().shape({});
+
+ return (
+
+
+
+
+ {(props) => (
+ {
+ e.preventDefault();
+ props.handleSubmit();
+ }}
+ >
+
+
+
+ {t("UpdateProfile.typography_edit_profile")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+ );
+};
+
+export default DashboardEditProfile;
diff --git a/src/components/dashboard/expert-management/DataTable/__tests__/index.test.js b/src/components/dashboard/expert-management/DataTable/__tests__/index.test.js
new file mode 100644
index 0000000..0ed26eb
--- /dev/null
+++ b/src/components/dashboard/expert-management/DataTable/__tests__/index.test.js
@@ -0,0 +1,33 @@
+import {render, screen} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../mocks/AppWithProvider";
+import ExpertManagementDataTable from "@/components/dashboard/expert-management/DataTable";
+
+describe("ExpertManagementDatatable Component From Expert Management", () => {
+ describe("Rendering", () => {
+ it("Table Headers Rendered", () => {
+ render();
+ const idHeader = screen.queryByText("کد یکتا");
+ const nameHeader = screen.queryByText("نام کامل");
+ const usernameHeader = screen.queryByText("نام کاربری");
+ const emailHeader = screen.queryByText("پست الکترونیک");
+ const phone_numberHeader = screen.queryByText("شماره همراه");
+ const national_idHeader = screen.queryByText("کد ملی");
+ const positionHeader = screen.queryByText("سمت");
+ const province_nameHeader = screen.queryByText("استان");
+ const role_nameHeader = screen.queryByText("نقش");
+ const genderHeader = screen.queryByText("جنسیت");
+ const updated_atHeader = screen.queryByText("آخرین بروزرسانی");
+ expect(idHeader).toBeInTheDocument();
+ expect(nameHeader).toBeInTheDocument();
+ expect(usernameHeader).toBeInTheDocument();
+ expect(emailHeader).toBeInTheDocument();
+ expect(phone_numberHeader).toBeInTheDocument();
+ expect(national_idHeader).toBeInTheDocument();
+ expect(positionHeader).toBeInTheDocument();
+ expect(province_nameHeader).toBeInTheDocument();
+ expect(role_nameHeader).toBeInTheDocument();
+ expect(genderHeader).toBeInTheDocument();
+ expect(updated_atHeader).toBeInTheDocument();
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/DataTable/index.jsx b/src/components/dashboard/expert-management/DataTable/index.jsx
new file mode 100644
index 0000000..3f76ca5
--- /dev/null
+++ b/src/components/dashboard/expert-management/DataTable/index.jsx
@@ -0,0 +1,190 @@
+import DataTable from "@/core/components/DataTable";
+import MuiDatePicker from "@/core/components/MuiDatePicker";
+import {GET_EXPERTS} from "@/core/data/apiRoutes";
+import TableToolbar from "../TableToolbar";
+import TableRowActions from "../TableRowActions";
+import {Box, Typography} from "@mui/material";
+import {useTranslations} from "next-intl";
+import {useMemo} from "react";
+import moment from "jalali-moment";
+
+function ExpertManagementDataTable() {
+ const t = useTranslations();
+ const columns = useMemo(
+ () => [
+ {
+ accessorFn: (row) => row.id,
+ id: "id",
+ header: t("ExpertMangement.id"),
+ enableColumnFilter: true,
+ datatype: "numeric",
+ filterFn: "equals",
+ columnFilterModeOptions: [
+ "equals",
+ "notEquals",
+ "contains",
+ "lessThan",
+ "greaterThan",
+ "between",
+ ],
+ Cell: ({renderedCellValue}) => (
+ {renderedCellValue}
+ ),
+ },
+ {
+ accessorFn: (row) => row.full_name,
+ id: "full_name",
+ header: t("ExpertMangement.name"),
+ enableColumnFilter: true,
+ datatype: "text",
+ filterFn: "contains",
+ columnFilterModeOptions: ["contains", "equals", "notEquals"],
+ Cell: ({renderedCellValue}) => (
+ {renderedCellValue}
+ ),
+ },
+ {
+ accessorFn: (row) => row.username,
+ id: "username",
+ header: t("ExpertMangement.username"),
+ enableColumnFilter: true,
+ datatype: "text",
+ filterFn: "contains",
+ columnFilterModeOptions: ["contains", "equals", "notEquals"],
+ Cell: ({renderedCellValue}) => (
+ {renderedCellValue}
+ ),
+ },
+ {
+ accessorFn: (row) => row.email,
+ id: "email",
+ header: t("ExpertMangement.email"),
+ enableColumnFilter: true,
+ datatype: "text",
+ filterFn: "contains",
+ columnFilterModeOptions: ["contains", "equals", "notEquals"],
+ Cell: ({renderedCellValue}) => (
+ {renderedCellValue}
+ ),
+ },
+ {
+ accessorFn: (row) => row.phone_number,
+ id: "phone_number",
+ header: t("ExpertMangement.phone_number"),
+ enableColumnFilter: true,
+ datatype: "numeric",
+ filterFn: "equals",
+ columnFilterModeOptions: ["contains", "equals", "notEquals"],
+ Cell: ({renderedCellValue}) => (
+ {renderedCellValue}
+ ),
+ },
+ {
+ accessorFn: (row) => row.national_id,
+ id: "national_id",
+ header: t("ExpertMangement.national_id"),
+ enableColumnFilter: true,
+ datatype: "numeric",
+ filterFn: "equals",
+ columnFilterModeOptions: ["contains", "equals", "notEquals"],
+ Cell: ({renderedCellValue}) => (
+ {renderedCellValue}
+ ),
+ },
+ {
+ accessorFn: (row) => row.position,
+ id: "position",
+ header: t("ExpertMangement.position"),
+ enableColumnFilter: true,
+ datatype: "text",
+ filterFn: "contains",
+ columnFilterModeOptions: ["contains", "equals", "notEquals"],
+ Cell: ({renderedCellValue}) => (
+ {renderedCellValue}
+ ),
+ },
+ {
+ accessorFn: (row) => row.province_fa,
+ id: "province_id",
+ header: t("ExpertMangement.province_fa"),
+ enableColumnFilter: false,
+ datatype: "text",
+ filterFn: "contains",
+ columnFilterModeOptions: ["contains", "equals", "notEquals"],
+ Cell: ({renderedCellValue}) => (
+ {renderedCellValue}
+ ),
+ },
+ {
+ accessorFn: (row) => row.roles[0]?.name_fa,
+ id: "roles[0].name_fa",
+ header: t("ExpertMangement.role_name"),
+ enableColumnFilter: false,
+ enableSorting: false,
+ datatype: "text",
+ filterFn: "contains",
+ columnFilterModeOptions: ["contains", "equals", "notEquals"],
+ Cell: ({renderedCellValue}) => (
+ {renderedCellValue}
+ ),
+ },
+ {
+ accessorFn: (row) => row.gender,
+ id: "gender",
+ header: t("ExpertMangement.gender"),
+ enableColumnFilter: false,
+ datatype: "text",
+ filterFn: "contains",
+ columnFilterModeOptions: ["contains", "equals", "notEquals"],
+ Cell: ({renderedCellValue}) => (
+ {renderedCellValue === "male" ? t("ExpertMangement.male") : t("ExpertMangement.female")}
+ ),
+ },
+ {
+ accessorFn: (row) =>
+ moment(row.updated_at).locale("fa").format("HH:mm | yyyy/MM/DD"),
+ id: "updated_at",
+ header: t("ExpertMangement.updated_at"),
+ enableColumnFilter: true,
+ datatype: "date",
+ filterFn: "lessThan",
+ columnFilterModeOptions: ["lessThan", "greaterThan"],
+ Cell: ({renderedCellValue}) => {
+ return {renderedCellValue};
+ },
+ Header: ({column}) => <>{column.columnDef.header}>,
+ Filter: ({column}) => {
+ return (
+
+ );
+ },
+ },
+ ],
+ []
+ );
+ return (
+
+
+
+ );
+}
+
+export default ExpertManagementDataTable;
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/ChangePassword/index.jsx b/src/components/dashboard/expert-management/Form/ChangePassword/index.jsx
new file mode 100644
index 0000000..ea3d11c
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/ChangePassword/index.jsx
@@ -0,0 +1,7 @@
+const ChangePassword = () => {
+ return (
+ <>>
+ )
+}
+
+export default ChangePassword
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/PersonalInfo/__tests__/index.test.js b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/PersonalInfo/__tests__/index.test.js
new file mode 100644
index 0000000..913c37a
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/PersonalInfo/__tests__/index.test.js
@@ -0,0 +1,219 @@
+import {act, fireEvent, render, screen, waitFor} from "@testing-library/react";
+import React from 'react';
+import MockAppWithProviders from "../../../../../../../../../mocks/AppWithProvider";
+import * as Yup from "yup";
+import {Formik} from "formik";
+import PersonalInfo from "@/components/dashboard/expert-management/Form/CreateForm/CreateContent/PersonalInfo";
+import {useTranslations} from "next-intl";
+import Province from "@/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/Province";
+
+
+const CreateFormMock = ({Component}) => {
+ const t = useTranslations();
+ const validationSchema = Yup.object().shape({
+ full_name: Yup.string().required(t("ExpertMangement.error_message_full_name")),
+ email: Yup.string().required(t("ExpertMangement.error_message_email")),
+ phone_number: Yup.string().required(t("ExpertMangement.error_message_phone_number")),
+ telephone_id: Yup.string().required(t("ExpertMangement.error_message_telephone_id")),
+ national_id: Yup.string().required(t("ExpertMangement.error_message_national_id")),
+ gender: Yup.string().required(t("ExpertMangement.error_message_gender"))
+ });
+ return (
+ {
+ }}
+ >
+ {formikProps => ()}
+
+ );
+};
+
+
+describe("PersonalInfo Component From Expert Management (create)", () => {
+ describe("Rendering", () => {
+ it("full_name TextField Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const fullNameTextField = screen.queryByLabelText('نام کامل');
+ expect(fullNameTextField).toBeInTheDocument();
+ });
+ });
+ it("Email TextField Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const emailTextField = screen.queryByLabelText('پست الکترونیک');
+ expect(emailTextField).toBeInTheDocument();
+ });
+ });
+ it("Phone Number TextField Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const phoneNumberTextField = screen.queryByLabelText('شماره همراه');
+ expect(phoneNumberTextField).toBeInTheDocument();
+ });
+ });
+ it("telephone id TextField Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const telephoneIdTextField = screen.queryByLabelText('کد تلفن');
+ expect(telephoneIdTextField).toBeInTheDocument();
+ });
+ });
+ it("National Id TextField Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const nationalIdTextField = screen.queryByLabelText('کد ملی');
+ expect(nationalIdTextField).toBeInTheDocument();
+ });
+ });
+ it("Gender Select Box Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const genderSelectBox = screen.getByTestId('select-box');
+ expect(genderSelectBox).toBeInTheDocument();
+ });
+ });
+ });
+ describe("Behavioral", () => {
+ it("Name TextField Work Correctly", async () => {
+ render();
+ const nameTextField = screen.queryByLabelText('نام کامل');
+ fireEvent.change(nameTextField, {target: {value: 'exampleName'}});
+ await act(() => {
+ expect(nameTextField).toHaveValue('exampleName');
+ })
+ });
+ it("Email TextField Work Correctly", async () => {
+ render();
+ const emailTextField = screen.queryByLabelText('پست الکترونیک');
+ fireEvent.change(emailTextField, {target: {value: 'exampleEmail'}});
+ await act(() => {
+ expect(emailTextField).toHaveValue('exampleEmail');
+ })
+ });
+ it("Phone Number TextField Work Correctly", async () => {
+ render();
+ const phoneNumberTextField = screen.queryByLabelText('شماره همراه');
+ fireEvent.change(phoneNumberTextField, {target: {value: 'examplePhoneNumber'}});
+ await act(() => {
+ expect(phoneNumberTextField).toHaveValue('examplePhoneNumber');
+ })
+ });
+ it("Telephone Id TextField Work Correctly", async () => {
+ render();
+ const telephoneIdTextField = screen.queryByLabelText('کد تلفن');
+ fireEvent.change(telephoneIdTextField, {target: {value: 'exampleTelephoneId'}});
+ await act(() => {
+ expect(telephoneIdTextField).toHaveValue('exampleTelephoneId');
+ })
+ });
+ it("National Id TextField Work Correctly", async () => {
+ render();
+ const nationalIdTextField = screen.queryByLabelText('کد ملی');
+ fireEvent.change(nationalIdTextField, {target: {value: 'exampleNationalId'}});
+ await act(() => {
+ expect(nationalIdTextField).toHaveValue('exampleNationalId');
+ })
+ });
+ it("Gender Select Box Work Correctly", async () => {
+ render();
+ const genderSelectBox = screen.getByTestId('select-box');
+ const genderSelectOpener = await waitFor(() => screen.getByTestId("option-opener-gender"));
+
+ await waitFor(() => {
+ expect(genderSelectBox).toBeInTheDocument();
+ });
+
+ fireEvent.mouseDown(genderSelectOpener);
+ await waitFor(() => {
+ const selectItem = screen.queryByText("مرد");
+ expect(selectItem).toBeInTheDocument();
+ });
+
+ });
+ });
+ describe("validation", () => {
+ it('Should See Error When Name Input Is Empty', async () => {
+ render();
+
+ const nameInput = screen.queryByLabelText('نام کامل');
+
+ fireEvent.change(nameInput, {target: {value: ''}});
+ fireEvent.blur(nameInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("نام کامل خود را وارد کنید")).toBeInTheDocument()
+ });
+ });
+ it('Should See Error When Email Input Is Empty', async () => {
+ render();
+
+ const emailInput = screen.queryByLabelText('پست الکترونیک');
+
+ fireEvent.change(emailInput, {target: {value: ''}});
+ fireEvent.blur(emailInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("پست الکترونیک خود را وارد کنید")).toBeInTheDocument()
+ });
+ });
+ it('Should See Error When Phone Number Input Is Empty', async () => {
+ render();
+
+ const phoneNumberInput = screen.queryByLabelText('شماره همراه');
+
+ fireEvent.change(phoneNumberInput, {target: {value: ''}});
+ fireEvent.blur(phoneNumberInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("شماره همراه خود را وارد کنید")).toBeInTheDocument()
+ });
+ });
+ it('Should See Error When telephone Id Input Is Empty', async () => {
+ render();
+
+ const telephoneIdTextField = screen.queryByLabelText('کد تلفن');
+
+ fireEvent.change(telephoneIdTextField, {target: {value: ''}});
+ fireEvent.blur(telephoneIdTextField);
+
+ await waitFor(() => {
+ expect(screen.queryByText("کد تلفن خود را وارد کنید")).toBeInTheDocument()
+ });
+ });
+ it('Should See Error When National Id Input Is Empty', async () => {
+ render();
+
+ const nationalIdInput = screen.queryByLabelText('کد ملی');
+
+ fireEvent.change(nationalIdInput, {target: {value: ''}});
+ fireEvent.blur(nationalIdInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("کد ملی خود را وارد کنید")).toBeInTheDocument()
+ });
+ });
+ it('Should Select An Item With Valid Value of gender', async () => {
+ render();
+ const genderInput = screen.getByTestId("input-gender")
+ const genderSelectOpener = await waitFor(() => screen.getByTestId("option-opener-gender"));
+
+ fireEvent.mouseDown(genderSelectOpener);
+ const selectItem = await waitFor(() => screen.queryByText("مرد"));
+ expect(selectItem).toBeInTheDocument();
+ fireEvent.click(selectItem);
+ await waitFor(() => {
+ expect(genderInput.value).toBe('male')
+ });
+ });
+ })
+});
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/PersonalInfo/index.jsx b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/PersonalInfo/index.jsx
new file mode 100644
index 0000000..f92bf38
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/PersonalInfo/index.jsx
@@ -0,0 +1,128 @@
+import {FormControl, FormHelperText, Grid, InputLabel, MenuItem, Select, TextField} from "@mui/material";
+import {useTranslations} from "next-intl";
+
+const PersonalInfo = ({formik}) => {
+ const t = useTranslations();
+
+ const genderList = [
+ {id: 1, name_en: "male", name_fa: "مرد"},
+ {id: 2, name_en: "female", name_fa: "زن"}
+ ]
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t("ExpertMangement.text_field_gender")}
+
+
+ {formik.touched.gender && formik.errors.gender ? formik.errors.gender : ""}
+
+
+
+
+ )
+}
+export default PersonalInfo
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/PositionAndRole/__tests__/index.test.js b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/PositionAndRole/__tests__/index.test.js
new file mode 100644
index 0000000..6f32bac
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/PositionAndRole/__tests__/index.test.js
@@ -0,0 +1,96 @@
+import {fireEvent, render, screen, waitFor} from "@testing-library/react";
+import React from 'react';
+import {useTranslations} from "next-intl";
+import * as Yup from "yup";
+import {Formik} from "formik";
+import MockAppWithProviders from "../../../../../../../../../../mocks/AppWithProvider";
+import PositionAndRole from "../../PositionAndRole";
+
+const CreateFormMock = ({Component}) => {
+ const t = useTranslations();
+ const validationSchema = Yup.object().shape({
+ position: Yup.string().required(t("ExpertMangement.error_message_position")),
+ roles: Yup.string().required(t("ExpertMangement.error_message_roles"))
+ });
+ return (
+ {
+ }}
+ >
+ {formikProps => ()}
+
+ );
+};
+
+describe("PositionAndRole Component From Expert Management (create)", () => {
+ describe("Rendering", () => {
+ it("Position TextField Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const positionTextField = screen.queryByLabelText('سمت');
+ expect(positionTextField).toBeInTheDocument();
+ });
+ });
+ it("Role Select Box Rendered", async () => {
+ render();
+ const roleSelect = screen.getByTestId('select-box');
+ await waitFor(() => {
+ expect(roleSelect).toBeInTheDocument();
+ });
+ });
+ });
+ describe("Behavioral", () => {
+ it("Position TextField Work Correctly", async () => {
+ render();
+ const positionTextField = screen.queryByLabelText('سمت');
+ fireEvent.change(positionTextField, {target: {value: 'examplePosition'}});
+ await waitFor(() => {
+ expect(positionTextField).toHaveValue('examplePosition');
+ })
+ });
+ it("Role Select Box Work Correctly", async () => {
+ render();
+ const roleSelect = screen.getByTestId("option-opener-role");
+ await waitFor(() => {
+ expect(roleSelect).toBeInTheDocument();
+ });
+ fireEvent.mouseDown(roleSelect);
+ await waitFor(() => {
+ const selectItem = screen.queryByText("ادمین");
+ expect(selectItem).toBeInTheDocument();
+ });
+ });
+ });
+ describe("validation", () => {
+ it('Should See Error When Position Input Is Empty', async () => {
+ render();
+
+ const positionInput = screen.queryByLabelText('سمت');
+
+ fireEvent.change(positionInput, {target: {value: ''}});
+ fireEvent.blur(positionInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("سمت خود را وارد کنید")).toBeInTheDocument()
+ });
+ });
+ it('Should Select An Item With Valid Value of role', async () => {
+ render();
+ const roleInput = await waitFor(() => screen.getByTestId("input-role-id"));
+ const roleSelectOpener = await waitFor(() => screen.getByTestId("option-opener-role"));
+
+ fireEvent.mouseDown(roleSelectOpener);
+ const selectItem = await waitFor(() => screen.queryByText("ادمین"));
+ expect(selectItem).toBeInTheDocument();
+ fireEvent.click(selectItem);
+ await waitFor(() => {
+ expect(roleInput.value).toBe('1')
+ });
+ });
+ })
+});
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/PositionAndRole/index.jsx b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/PositionAndRole/index.jsx
new file mode 100644
index 0000000..6befbff
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/PositionAndRole/index.jsx
@@ -0,0 +1,74 @@
+import {FormControl, FormHelperText, Grid, InputLabel, MenuItem, Select, TextField} from "@mui/material";
+import {useTranslations} from "next-intl";
+import useRequest from "@/lib/app/hooks/useRequest";
+import useRole from "@/lib/app/hooks/useRole";
+import {log} from "next/dist/server/typescript/utils";
+
+const PositionAndRole = ({formik}) => {
+ const t = useTranslations();
+ const requestServer = useRequest()
+ const {roleList, isLoadingRoleList, errorRoleList} = useRole();
+
+ return (
+
+
+
+
+
+
+ {t("ExpertMangement.text_field_roles")}
+
+
+ {formik.touched.roles && formik.errors.roles ? formik.errors.roles : ""}
+
+
+
+
+ )
+}
+export default PositionAndRole
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/Province/__tests__/index.test.js b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/Province/__tests__/index.test.js
new file mode 100644
index 0000000..87da19f
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/Province/__tests__/index.test.js
@@ -0,0 +1,71 @@
+import {fireEvent, render, screen, waitFor} from "@testing-library/react";
+import React from 'react';
+import {useTranslations} from "next-intl";
+import * as Yup from "yup";
+import {Formik} from "formik";
+import Province from "../../Province";
+import MockAppWithProviders from "../../../../../../../../../../mocks/AppWithProvider";
+
+const CreateFormMock = ({Component}) => {
+ const t = useTranslations();
+ const validationSchema = Yup.object().shape({
+ province_id: Yup.string().required(t("ExpertMangement.error_message_province_id")),
+ });
+ return (
+ {
+ }}
+ >
+ {formikProps => ()}
+
+ );
+};
+
+describe("Province Component From Expert Management (create)", () => {
+ describe("Rendering", () => {
+ it("Province Select Box Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const provinceSelectBox = screen.getByTestId('select-box');
+ expect(provinceSelectBox).toBeInTheDocument();
+ });
+ });
+ });
+ describe("Behavioral", () => {
+ it("Province Select Box Work Correctly", async () => {
+ render();
+ const provinceSelectBox = screen.getByTestId('select-box');
+ const provinceSelectOpener = await waitFor(() => screen.getByTestId("option-opener-province"));
+
+ await waitFor(() => {
+ expect(provinceSelectBox).toBeInTheDocument();
+ });
+
+ fireEvent.mouseDown(provinceSelectOpener);
+ await waitFor(() => {
+ const selectItem = screen.queryByText("آذربایجان شرقی");
+ expect(selectItem).toBeInTheDocument();
+ });
+
+ });
+ });
+ describe("validation", () => {
+ it('Should Select An Item With Valid Value of province', async () => {
+ render();
+ const provinceInput = screen.getByTestId("input-province-id")
+ const provinceSelectOpener = await waitFor(() => screen.getByTestId("option-opener-province"));
+
+ fireEvent.mouseDown(provinceSelectOpener);
+ const selectItem = await waitFor(() => screen.queryByText("آذربایجان شرقی"));
+ expect(selectItem).toBeInTheDocument();
+ fireEvent.click(selectItem);
+ await waitFor(() => {
+ expect(provinceInput.value).toBe('1')
+ });
+ });
+ })
+});
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/Province/index.jsx b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/Province/index.jsx
new file mode 100644
index 0000000..5ac94b2
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/Province/index.jsx
@@ -0,0 +1,61 @@
+import {FormControl, FormHelperText, Grid, InputLabel, MenuItem, Select} from "@mui/material";
+import {useTranslations} from "next-intl";
+import useProvince from "@/lib/app/hooks/useProvince";
+import useRequest from "@/lib/app/hooks/useRequest";
+
+const Province = ({formik}) => {
+ const t = useTranslations();
+ const requestServer = useRequest()
+ const {provinceList, isLoadingProvinceList, errorProvinceList} = useProvince();
+
+ return (
+
+
+
+ {t("ExpertMangement.text_field_province_id")}
+
+
+ {formik.touched.province_id && formik.errors.province_id ? formik.errors.province_id : ""}
+
+
+
+
+ )
+}
+export default Province
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/index.jsx b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/index.jsx
new file mode 100644
index 0000000..072c096
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/index.jsx
@@ -0,0 +1,12 @@
+import Province from "./Province";
+import PositionAndRole from "./PositionAndRole";
+
+const RestInfo = ({formik}) => {
+ return (
+ <>
+
+
+ >
+ )
+}
+export default RestInfo
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/UserInfo/__test__/index.test.js b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/UserInfo/__test__/index.test.js
new file mode 100644
index 0000000..519cf9d
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/UserInfo/__test__/index.test.js
@@ -0,0 +1,123 @@
+import {act, fireEvent, render, screen, waitFor} from "@testing-library/react";
+import React from 'react';
+import MockAppWithProviders from "../../../../../../../../../mocks/AppWithProvider";
+import {useTranslations} from "next-intl";
+import * as Yup from "yup";
+import {Formik} from "formik";
+import UserInfo from "../../UserInfo";
+
+const CreateFormMock = ({Component}) => {
+ const t = useTranslations();
+ const validationSchema = Yup.object().shape({
+ username: Yup.string().required(t("ExpertMangement.error_message_username")),
+ password: Yup.string()
+ .required(t("ExpertMangement.error_message_password"))
+ .matches(
+ /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/,
+ t("ExpertMangement.error_message_password_regex")
+ ),
+ });
+ return (
+ {
+ }}
+ >
+ {formikProps => ()}
+
+ );
+};
+
+describe("UserInfo Component From Expert Management (create)", () => {
+ describe("Rendering", () => {
+ it("UserName TextField Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const usernameTextField = screen.queryByLabelText('نام کاربری');
+ expect(usernameTextField).toBeInTheDocument();
+ });
+ });
+ it("Password TextField Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const passwordTextField = screen.queryByLabelText('رمز عبور');
+ expect(passwordTextField).toBeInTheDocument();
+ });
+
+ });
+ });
+ describe("Behavioral", () => {
+ it("Username TextField Work Correctly", async () => {
+ render();
+ const usernameTextField = screen.queryByLabelText('نام کاربری');
+ fireEvent.change(usernameTextField, {target: {value: 'exampleUsername'}});
+ await act(() => {
+ expect(usernameTextField).toHaveValue('exampleUsername');
+ })
+ });
+ it("Password TextField Work Correctly", async () => {
+ render();
+ const passwordTextField = screen.queryByLabelText('رمز عبور');
+ fireEvent.change(passwordTextField, {target: {value: 'examplePassword'}});
+ await act(() => {
+ expect(passwordTextField).toHaveValue('examplePassword');
+ })
+ });
+ });
+ describe("validation", () => {
+ it('Should See Error When Username Input Is Empty', async () => {
+ render();
+
+ const usernameInput = screen.queryByLabelText('نام کاربری');
+
+ fireEvent.change(usernameInput, {target: {value: ''}});
+ fireEvent.blur(usernameInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("نام کاربری خود را وارد کنید")).toBeInTheDocument()
+ });
+ });
+ it('Should See Error When Password Input Is Empty', async () => {
+ render();
+
+ const passwordInput = screen.queryByLabelText('رمز عبور');
+
+ fireEvent.change(passwordInput, {target: {value: ''}});
+ fireEvent.blur(passwordInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("رمز عبور خود را وارد کنید")).toBeInTheDocument()
+ });
+ });
+ it('Should See Error When Password Field Is Not On Correct Format', async () => {
+ render();
+
+ const passwordInput = screen.queryByLabelText('رمز عبور');
+
+ // check without text or symbol
+ fireEvent.change(passwordInput, {target: {value: '12345678'}});
+ fireEvent.blur(passwordInput);
+ await waitFor(() => {
+ expect(screen.queryByText("رمز عبور باید حداقل 8 کاراکتر و متشکل از عدد، حرف یا سمبل باشد")).toBeInTheDocument()
+ });
+
+ // check without number
+ fireEvent.change(passwordInput, {target: {value: 'abcdefgh'}});
+ fireEvent.blur(passwordInput);
+ await waitFor(() => {
+ expect(screen.queryByText("رمز عبور باید حداقل 8 کاراکتر و متشکل از عدد، حرف یا سمبل باشد")).toBeInTheDocument()
+ });
+
+ // check under 8 character
+ fireEvent.change(passwordInput, {target: {value: '11Sa'}});
+ fireEvent.blur(passwordInput);
+ await waitFor(() => {
+ expect(screen.queryByText("رمز عبور باید حداقل 8 کاراکتر و متشکل از عدد، حرف یا سمبل باشد")).toBeInTheDocument()
+ });
+ });
+ })
+});
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/UserInfo/index.jsx b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/UserInfo/index.jsx
new file mode 100644
index 0000000..2455b26
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/UserInfo/index.jsx
@@ -0,0 +1,61 @@
+import {Grid, IconButton, InputAdornment, TextField} from "@mui/material";
+import {useTranslations} from "next-intl";
+import {Visibility, VisibilityOff} from "@mui/icons-material";
+import {useState} from "react";
+
+const UserInfo = ({formik}) => {
+ const t = useTranslations();
+ const [showPassword, setShowPassword] = useState(false);
+
+ const handleClickShowPassword = () => {
+ setShowPassword(!showPassword);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {showPassword ? : }
+
+
+ ),
+ }}
+ sx={{width: "100%"}}
+ />
+
+
+ >
+ )
+}
+export default UserInfo
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/__tests__/index.test.js b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/__tests__/index.test.js
new file mode 100644
index 0000000..85751c5
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/__tests__/index.test.js
@@ -0,0 +1,67 @@
+import {fireEvent, render, screen, waitFor} from "@testing-library/react";
+import React from 'react';
+import CreateContent from "../../CreateContent";
+import MockAppWithProviders from "../../../../../../../../mocks/AppWithProvider";
+
+function selectDropdownItem(screen, openerTestId, itemText) {
+ const opener = screen.getByTestId(openerTestId);
+ fireEvent.mouseDown(opener);
+ const selectItem = screen.queryByText(itemText);
+ fireEvent.click(selectItem);
+}
+
+function setInputValue(screen, inputElement, value) {
+ fireEvent.change(inputElement, {target: {value}});
+}
+
+describe("CreateContent Component From Expert Management (create)", () => {
+ describe("Rendering", () => {
+ it("close button Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const cancelBtn = screen.queryByText('بستن');
+ expect(cancelBtn).toBeInTheDocument();
+ });
+ });
+ it("confirm button Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const cancelBtn = screen.queryByText('ثبت');
+ expect(cancelBtn).toBeInTheDocument();
+ });
+ });
+ });
+ describe("Form Submission", () => {
+ it('Should enable the submit button when the inputs are valid', async () => {
+ render();
+
+ const submitButton = screen.queryByText('ثبت');
+
+ const nameInput = screen.queryByLabelText('نام کامل');
+ const usernameInput = screen.queryByLabelText('نام کاربری');
+ const emailInput = screen.queryByLabelText('پست الکترونیک');
+ const phoneNumberInput = screen.queryByLabelText('شماره همراه');
+ const telephoneIdInput = screen.queryByLabelText('کد تلفن');
+ const nationalIdInput = screen.queryByLabelText('کد ملی');
+ const passwordInput = screen.queryByLabelText('رمز عبور');
+ const positionInput = screen.queryByLabelText('سمت');
+
+ selectDropdownItem(screen, "option-opener-province", "آذربایجان شرقی");
+ selectDropdownItem(screen, "option-opener-role", "ادمین");
+ selectDropdownItem(screen, "option-opener-gender", "مرد");
+
+ setInputValue(screen, nameInput, 'nameTest');
+ setInputValue(screen, usernameInput, 'usernameTest');
+ setInputValue(screen, emailInput, 'emailTest');
+ setInputValue(screen, phoneNumberInput, 'phoneNumberTest');
+ setInputValue(screen, telephoneIdInput, 'telephoneIdTest');
+ setInputValue(screen, nationalIdInput, 'nationalIdTest');
+ setInputValue(screen, passwordInput, 'passwordTest');
+ setInputValue(screen, positionInput, 'positionTest');
+
+ await waitFor(() => {
+ expect(submitButton).not.toBeDisabled();
+ });
+ });
+ })
+});
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/index.jsx b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/index.jsx
new file mode 100644
index 0000000..084da99
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/CreateForm/CreateContent/index.jsx
@@ -0,0 +1,116 @@
+import {Box, Button, Chip, DialogActions, DialogContent, Divider,} from "@mui/material";
+import * as Yup from "yup";
+import {useTranslations} from "next-intl";
+import {useFormik} from "formik";
+import useRequest from "@/lib/app/hooks/useRequest";
+import {ADD_EXPERT} from "@/core/data/apiRoutes";
+import PersonalInfo from "@/components/dashboard/expert-management/Form/CreateForm/CreateContent/PersonalInfo";
+import UserInfo from "@/components/dashboard/expert-management/Form/CreateForm/CreateContent/UserInfo";
+import RestInfo from "@/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo";
+
+const CreateContent = ({setOpenCreateDialog, mutate}) => {
+ const t = useTranslations();
+ const requestServer = useRequest()
+
+ const validationSchema = Yup.object().shape({
+ full_name: Yup.string().required(t("ExpertMangement.error_message_full_name")),
+ username: Yup.string().required(t("ExpertMangement.error_message_username")),
+ email: Yup.string().required(t("ExpertMangement.error_message_email")),
+ phone_number: Yup.string().required(t("ExpertMangement.error_message_phone_number")),
+ telephone_id: Yup.string().required(t("ExpertMangement.error_message_telephone_id")),
+ gender: Yup.string().required(t("ExpertMangement.error_message_gender")),
+ national_id: Yup.string().required(t("ExpertMangement.error_message_national_id")),
+ password: Yup.string()
+ .required(t("ExpertMangement.error_message_password"))
+ .matches(
+ /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/,
+ t("ExpertMangement.error_message_password_regex")
+ ),
+ position: Yup.string().required(t("ExpertMangement.error_message_position")),
+ province_id: Yup.string().required(t("ExpertMangement.error_message_province_id")),
+ roles: Yup.string().required(t("ExpertMangement.error_message_roles"))
+ });
+
+ const formik = useFormik({
+ initialValues: {
+ full_name: "",
+ username: "",
+ email: "",
+ phone_number: "",
+ telephone_id: "",
+ gender: "",
+ national_id: "",
+ password: "",
+ position: "",
+ province_id: "",
+ roles: ""
+ },
+ validationSchema,
+ onSubmit: (values, {setSubmitting}) => {
+ const formData = new FormData();
+ formData.append("full_name", values.full_name);
+ formData.append("national_id", values.national_id);
+ formData.append("phone_number", values.phone_number);
+ formData.append("telephone_id", values.telephone_id);
+ formData.append("gender", values.gender);
+ formData.append("email", values.email);
+ formData.append("username", values.username);
+ formData.append("password", values.password);
+ formData.append("province_id", values.province_id);
+ formData.append("position", values.position);
+ formData.append("roles", values.roles);
+
+ requestServer(`${ADD_EXPERT}`, 'post', {auth: true, data: formData})
+ .then((response) => {
+ setOpenCreateDialog(false)
+ mutate()
+ }).catch(() => {
+ }).finally(() => {
+ setSubmitting(false);
+ });
+ },
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default CreateContent
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/CreateForm/__tests__/index.test.js b/src/components/dashboard/expert-management/Form/CreateForm/__tests__/index.test.js
new file mode 100644
index 0000000..8bf825d
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/CreateForm/__tests__/index.test.js
@@ -0,0 +1,131 @@
+import {act, fireEvent, render, screen, waitFor} from "@testing-library/react";
+import React from 'react';
+import MockAppWithProviders from "../../../../../../../mocks/AppWithProvider";
+import CreateForm from "../../CreateForm";
+
+function selectDropdownItem(screen, openerTestId, itemText) {
+ const opener = screen.getByTestId(openerTestId);
+ fireEvent.mouseDown(opener);
+ const selectItem = screen.queryByText(itemText);
+ fireEvent.click(selectItem);
+}
+
+function setInputValue(screen, inputElement, value) {
+ fireEvent.change(inputElement, {target: {value}});
+}
+
+describe("CreateForm Component From Expert Management", () => {
+ describe("Rendering", () => {
+ it("Create Button Rendered", () => {
+ render();
+ const CreateButton = screen.getByRole('button', {name: "افزودن کارشناس"});
+ expect(CreateButton).toBeInTheDocument();
+ });
+ it("Dialog Header Rendered", () => {
+ render();
+ const CreateDialogHeader = screen.queryByText("افزودن کارشناس");
+ expect(CreateDialogHeader).toBeInTheDocument();
+ });
+ });
+ describe("Behavioral", () => {
+ it("by Clicking Create Button Dialog Should Append To Document", async () => {
+ render();
+ const CreateButton = screen.getByRole('button', {name: "افزودن کارشناس"});
+ fireEvent.click(CreateButton);
+ await act(() => {
+ const CreateDialog = screen.getByRole('dialog', {name: "افزودن کارشناس"})
+ expect(CreateDialog).toBeInTheDocument();
+ });
+ });
+ });
+ describe("Form Submission", () => {
+ it('Should request to api and if get success close dialog', async () => {
+ render();
+
+ const CreateButton = screen.getByRole('button', {name: "افزودن کارشناس"});
+ fireEvent.click(CreateButton);
+
+ const submitButton = screen.queryByText('ثبت');
+
+ const nameInput = screen.queryByLabelText('نام کامل');
+ const usernameInput = screen.queryByLabelText('نام کاربری');
+ const emailInput = screen.queryByLabelText('پست الکترونیک');
+ const phoneNumberInput = screen.queryByLabelText('شماره همراه');
+ const telephoneIdInput = screen.queryByLabelText('کد تلفن');
+ const nationalIdInput = screen.queryByLabelText('کد ملی');
+ const passwordInput = screen.queryByLabelText('رمز عبور');
+ const positionInput = screen.queryByLabelText('سمت');
+
+ selectDropdownItem(screen, "option-opener-province", "آذربایجان شرقی");
+ selectDropdownItem(screen, "option-opener-role", "ادمین");
+ selectDropdownItem(screen, "option-opener-gender", "مرد");
+
+ setInputValue(screen, nameInput, 'nameTest');
+ setInputValue(screen, usernameInput, 'usernameTest');
+ setInputValue(screen, emailInput, 'emailTest@gmail.com');
+ setInputValue(screen, phoneNumberInput, '0914577458');
+ setInputValue(screen, telephoneIdInput, '091');
+ setInputValue(screen, nationalIdInput, 'nationalIdTest');
+ setInputValue(screen, passwordInput, 'passwordTest12');
+ setInputValue(screen, positionInput, 'positionTest');
+
+ expect(submitButton).not.toBeDisabled();
+ fireEvent.click(submitButton);
+
+ await waitFor(() => {
+ const dialogContent = screen.queryByTestId('create-dialog-content');
+ expect(dialogContent).not.toBeInTheDocument();
+ });
+ });
+ it('Should request to api and if get error keep previous data', async () => {
+ render();
+
+ const CreateButton = screen.getByRole('button', {name: "افزودن کارشناس"});
+ fireEvent.click(CreateButton);
+
+ const submitButton = screen.queryByText('ثبت');
+
+ const nameInput = screen.queryByLabelText('نام کامل');
+ const usernameInput = screen.queryByLabelText('نام کاربری');
+ const emailInput = screen.queryByLabelText('پست الکترونیک');
+ const phoneNumberInput = screen.queryByLabelText('شماره همراه');
+ const telephoneIdInput = screen.queryByLabelText('کد تلفن');
+ const nationalIdInput = screen.queryByLabelText('کد ملی');
+ const passwordInput = screen.queryByLabelText('رمز عبور');
+ const positionInput = screen.queryByLabelText('سمت');
+ const provinceInput = screen.getByTestId("input-province-id")
+ const roleInput = screen.getByTestId("input-role-id")
+ const genderInput = screen.getByTestId("input-gender")
+
+ selectDropdownItem(screen, "option-opener-province", "آذربایجان شرقی");
+ selectDropdownItem(screen, "option-opener-role", "ادمین");
+ selectDropdownItem(screen, "option-opener-gender", "مرد");
+
+ setInputValue(screen, nameInput, 'nameTest');
+ setInputValue(screen, usernameInput, 'usernameTest');
+ setInputValue(screen, emailInput, 'emailTest@gmail.com');
+ setInputValue(screen, phoneNumberInput, '0914577458');
+ setInputValue(screen, telephoneIdInput, '091');
+ setInputValue(screen, nationalIdInput, 'nationalIdTest');
+ setInputValue(screen, passwordInput, 'passwordTest12');
+ setInputValue(screen, positionInput, 'positionTest');
+
+ expect(submitButton).not.toBeDisabled();
+ await fireEvent.click(submitButton);
+
+ await waitFor(() => {
+ expect(nameInput).toHaveValue('nameTest');
+ expect(usernameInput).toHaveValue('usernameTest');
+ expect(emailInput).toHaveValue('emailTest@gmail.com');
+ expect(phoneNumberInput).toHaveValue('0914577458');
+ expect(telephoneIdInput).toHaveValue('091');
+ expect(nationalIdInput).toHaveValue('nationalIdTest');
+ expect(passwordInput).toHaveValue('passwordTest12');
+ expect(positionInput).toHaveValue('positionTest');
+ expect(provinceInput).toHaveValue('1');
+ expect(roleInput).toHaveValue('1');
+ expect(genderInput).toHaveValue('male');
+ });
+ });
+ })
+});
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/CreateForm/index.jsx b/src/components/dashboard/expert-management/Form/CreateForm/index.jsx
new file mode 100644
index 0000000..92f822f
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/CreateForm/index.jsx
@@ -0,0 +1,39 @@
+import {Button, Dialog, DialogTitle, Stack, Tooltip} from "@mui/material";
+import {useTranslations} from "next-intl";
+import DataSaverOnIcon from "@mui/icons-material/DataSaverOn";
+import {useState} from "react";
+import CreateContent from "./CreateContent";
+
+const Create = ({mutate}) => {
+ const t = useTranslations();
+ const [openCreateDialog, setOpenCreateDialog] = useState(false);
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
+
+export default Create
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/DeleteForm/__tests__/index.test.js b/src/components/dashboard/expert-management/Form/DeleteForm/__tests__/index.test.js
new file mode 100644
index 0000000..f2e52b0
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/DeleteForm/__tests__/index.test.js
@@ -0,0 +1,66 @@
+import {fireEvent, render, screen, waitFor} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../../mocks/AppWithProvider";
+import React from "react";
+import DeleteForm from "@/components/dashboard/expert-management/Form/DeleteForm";
+
+describe("CreateForm Component From Expert Management", () => {
+ describe("Rendering", () => {
+ it("Delete Expert Button And Tooltip Rendered", () => {
+ render();
+ const DeleteButton = screen.getByTestId('delete-button');
+ expect(DeleteButton).toBeInTheDocument();
+ });
+ it("Delete Dialog Rendered", () => {
+ render();
+ const DeleteButton = screen.getByTestId('delete-button');
+ fireEvent.click(DeleteButton);
+ const DeleteDialogHeader = screen.queryByText("حذف کارشناس");
+ expect(DeleteDialogHeader).toBeInTheDocument();
+ });
+ it("Delete Dialog Text Rendered", () => {
+ render();
+ const DeleteButton = screen.getByTestId('delete-button');
+ fireEvent.click(DeleteButton);
+ const DeleteDialogText = screen.queryByText("آیا از انجام این عملیات اطمینان دارید؟");
+ expect(DeleteDialogText).toBeInTheDocument();
+ });
+ it("Delete Dialog Delete Button Rendered", () => {
+ render();
+ const DeleteButton = screen.getByTestId('delete-button');
+ fireEvent.click(DeleteButton);
+ const DeleteButtonDialog = screen.getByRole('button', {name: "حذف"});
+ expect(DeleteButtonDialog).toBeInTheDocument();
+ });
+ it("Delete Dialog Cancel Button Rendered", () => {
+ render();
+ const DeleteButton = screen.getByTestId('delete-button');
+ fireEvent.click(DeleteButton);
+ const CancelButtonDialog = screen.getByRole('button', {name: "بستن"});
+ expect(CancelButtonDialog).toBeInTheDocument();
+ });
+ });
+ describe("Behavioral", () => {
+ it("By Clicking Delete Button Dialog Should Append To Document", async () => {
+ render();
+ const DeleteButton = screen.getByTestId('delete-button');
+ fireEvent.click(DeleteButton);
+ await waitFor(() => {
+ const DeleteDialog = screen.getByRole('dialog', {name: "حذف کارشناس"})
+ expect(DeleteDialog).toBeInTheDocument();
+ });
+ });
+ });
+ describe("Form Submission", () => {
+ it("By Clicking Delete Button Request To Api And Delete Item", async () => {
+ render();
+ const DeleteButton = screen.getByTestId('delete-button');
+ fireEvent.click(DeleteButton);
+ const DeleteButtonDialog = screen.getByRole('button', {name: "حذف"});
+ fireEvent.click(DeleteButtonDialog);
+ await waitFor(() => {
+ const DeleteDialog = screen.queryByRole('dialog', {name: "حذف کارشناس"});
+ expect(DeleteDialog).not.toBeInTheDocument();
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/DeleteForm/index.jsx b/src/components/dashboard/expert-management/Form/DeleteForm/index.jsx
new file mode 100644
index 0000000..0d8e592
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/DeleteForm/index.jsx
@@ -0,0 +1,74 @@
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle,
+ IconButton,
+ Tooltip
+} from "@mui/material";
+import DeleteIcon from '@mui/icons-material/Delete';
+import {useState} from "react";
+import {DELETE_EXPERT} from "@/core/data/apiRoutes";
+import useRequest from "@/lib/app/hooks/useRequest";
+import {useTranslations} from "next-intl";
+
+const Delete = ({rowId, mutate}) => {
+ const t = useTranslations();
+ const requestServer = useRequest({auth: true});
+ const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const handleDelete = () => {
+ setIsSubmitting(true)
+ requestServer(`${DELETE_EXPERT}/${rowId}`, 'DELETE')
+ .then((response) => {
+ setOpenDeleteDialog(false);
+ mutate()
+ })
+ .catch(() => {
+ })
+ .finally(() => {
+ setIsSubmitting(false)
+ });
+ };
+
+ return (
+ <>
+
+ {
+ setOpenDeleteDialog(true);
+ }}
+ >
+
+
+
+
+ >
+ );
+};
+export default Delete;
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/PersonalInfo/__tests__/index.test.js b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/PersonalInfo/__tests__/index.test.js
new file mode 100644
index 0000000..b8692d5
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/PersonalInfo/__tests__/index.test.js
@@ -0,0 +1,219 @@
+import {act, fireEvent, render, screen, waitFor} from "@testing-library/react";
+import React from 'react';
+import MockAppWithProviders from "../../../../../../../../../mocks/AppWithProvider";
+import * as Yup from "yup";
+import {Formik} from "formik";
+import PersonalInfo from "@/components/dashboard/expert-management/Form/CreateForm/CreateContent/PersonalInfo";
+import {useTranslations} from "next-intl";
+import Province from "@/components/dashboard/expert-management/Form/CreateForm/CreateContent/RestInfo/Province";
+
+
+const CreateFormMock = ({Component}) => {
+ const t = useTranslations();
+ const validationSchema = Yup.object().shape({
+ full_name: Yup.string().required(t("ExpertMangement.error_message_full_name")),
+ email: Yup.string().required(t("ExpertMangement.error_message_email")),
+ phone_number: Yup.string().required(t("ExpertMangement.error_message_phone_number")),
+ telephone_id: Yup.string().required(t("ExpertMangement.error_message_telephone_id")),
+ national_id: Yup.string().required(t("ExpertMangement.error_message_national_id")),
+ gender: Yup.string().required(t("ExpertMangement.error_message_gender"))
+ });
+ return (
+ {
+ }}
+ >
+ {formikProps => ()}
+
+ );
+};
+
+
+describe("PersonalInfo Component From Expert Management (update)", () => {
+ describe("Rendering", () => {
+ it("full_name TextField Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const fullNameTextField = screen.queryByLabelText('نام کامل');
+ expect(fullNameTextField).toBeInTheDocument();
+ });
+ });
+ it("Email TextField Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const emailTextField = screen.queryByLabelText('پست الکترونیک');
+ expect(emailTextField).toBeInTheDocument();
+ });
+ });
+ it("Phone Number TextField Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const phoneNumberTextField = screen.queryByLabelText('شماره همراه');
+ expect(phoneNumberTextField).toBeInTheDocument();
+ });
+ });
+ it("telephone id TextField Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const telephoneIdTextField = screen.queryByLabelText('کد تلفن');
+ expect(telephoneIdTextField).toBeInTheDocument();
+ });
+ });
+ it("National Id TextField Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const nationalIdTextField = screen.queryByLabelText('کد ملی');
+ expect(nationalIdTextField).toBeInTheDocument();
+ });
+ });
+ it("Gender Select Box Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const genderSelectBox = screen.getByTestId('select-box');
+ expect(genderSelectBox).toBeInTheDocument();
+ });
+ });
+ });
+ describe("Behavioral", () => {
+ it("Name TextField Work Correctly", async () => {
+ render();
+ const nameTextField = screen.queryByLabelText('نام کامل');
+ fireEvent.change(nameTextField, {target: {value: 'exampleName'}});
+ await act(() => {
+ expect(nameTextField).toHaveValue('exampleName');
+ })
+ });
+ it("Email TextField Work Correctly", async () => {
+ render();
+ const emailTextField = screen.queryByLabelText('پست الکترونیک');
+ fireEvent.change(emailTextField, {target: {value: 'exampleEmail'}});
+ await act(() => {
+ expect(emailTextField).toHaveValue('exampleEmail');
+ })
+ });
+ it("Phone Number TextField Work Correctly", async () => {
+ render();
+ const phoneNumberTextField = screen.queryByLabelText('شماره همراه');
+ fireEvent.change(phoneNumberTextField, {target: {value: 'examplePhoneNumber'}});
+ await act(() => {
+ expect(phoneNumberTextField).toHaveValue('examplePhoneNumber');
+ })
+ });
+ it("Telephone Id TextField Work Correctly", async () => {
+ render();
+ const telephoneIdTextField = screen.queryByLabelText('کد تلفن');
+ fireEvent.change(telephoneIdTextField, {target: {value: 'exampleTelephoneId'}});
+ await act(() => {
+ expect(telephoneIdTextField).toHaveValue('exampleTelephoneId');
+ })
+ });
+ it("National Id TextField Work Correctly", async () => {
+ render();
+ const nationalIdTextField = screen.queryByLabelText('کد ملی');
+ fireEvent.change(nationalIdTextField, {target: {value: 'exampleNationalId'}});
+ await act(() => {
+ expect(nationalIdTextField).toHaveValue('exampleNationalId');
+ })
+ });
+ it("Gender Select Box Work Correctly", async () => {
+ render();
+ const genderSelectBox = screen.getByTestId('select-box');
+ const genderSelectOpener = await waitFor(() => screen.getByTestId("option-opener-gender"));
+
+ await waitFor(() => {
+ expect(genderSelectBox).toBeInTheDocument();
+ });
+
+ fireEvent.mouseDown(genderSelectOpener);
+ await waitFor(() => {
+ const selectItem = screen.queryByText("مرد");
+ expect(selectItem).toBeInTheDocument();
+ });
+
+ });
+ });
+ describe("validation", () => {
+ it('Should See Error When Name Input Is Empty', async () => {
+ render();
+
+ const nameInput = screen.queryByLabelText('نام کامل');
+
+ fireEvent.change(nameInput, {target: {value: ''}});
+ fireEvent.blur(nameInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("نام کامل خود را وارد کنید")).toBeInTheDocument()
+ });
+ });
+ it('Should See Error When Email Input Is Empty', async () => {
+ render();
+
+ const emailInput = screen.queryByLabelText('پست الکترونیک');
+
+ fireEvent.change(emailInput, {target: {value: ''}});
+ fireEvent.blur(emailInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("پست الکترونیک خود را وارد کنید")).toBeInTheDocument()
+ });
+ });
+ it('Should See Error When Phone Number Input Is Empty', async () => {
+ render();
+
+ const phoneNumberInput = screen.queryByLabelText('شماره همراه');
+
+ fireEvent.change(phoneNumberInput, {target: {value: ''}});
+ fireEvent.blur(phoneNumberInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("شماره همراه خود را وارد کنید")).toBeInTheDocument()
+ });
+ });
+ it('Should See Error When telephone Id Input Is Empty', async () => {
+ render();
+
+ const telephoneIdTextField = screen.queryByLabelText('کد تلفن');
+
+ fireEvent.change(telephoneIdTextField, {target: {value: ''}});
+ fireEvent.blur(telephoneIdTextField);
+
+ await waitFor(() => {
+ expect(screen.queryByText("کد تلفن خود را وارد کنید")).toBeInTheDocument()
+ });
+ });
+ it('Should See Error When National Id Input Is Empty', async () => {
+ render();
+
+ const nationalIdInput = screen.queryByLabelText('کد ملی');
+
+ fireEvent.change(nationalIdInput, {target: {value: ''}});
+ fireEvent.blur(nationalIdInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("کد ملی خود را وارد کنید")).toBeInTheDocument()
+ });
+ });
+ it('Should Select An Item With Valid Value of gender', async () => {
+ render();
+ const genderInput = screen.getByTestId("input-gender")
+ const genderSelectOpener = await waitFor(() => screen.getByTestId("option-opener-gender"));
+
+ fireEvent.mouseDown(genderSelectOpener);
+ const selectItem = await waitFor(() => screen.queryByText("مرد"));
+ expect(selectItem).toBeInTheDocument();
+ fireEvent.click(selectItem);
+ await waitFor(() => {
+ expect(genderInput.value).toBe('male')
+ });
+ });
+ })
+});
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/PersonalInfo/index.jsx b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/PersonalInfo/index.jsx
new file mode 100644
index 0000000..f92bf38
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/PersonalInfo/index.jsx
@@ -0,0 +1,128 @@
+import {FormControl, FormHelperText, Grid, InputLabel, MenuItem, Select, TextField} from "@mui/material";
+import {useTranslations} from "next-intl";
+
+const PersonalInfo = ({formik}) => {
+ const t = useTranslations();
+
+ const genderList = [
+ {id: 1, name_en: "male", name_fa: "مرد"},
+ {id: 2, name_en: "female", name_fa: "زن"}
+ ]
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t("ExpertMangement.text_field_gender")}
+
+
+ {formik.touched.gender && formik.errors.gender ? formik.errors.gender : ""}
+
+
+
+
+ )
+}
+export default PersonalInfo
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/PositionAndRole/__tests__/index.test.js b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/PositionAndRole/__tests__/index.test.js
new file mode 100644
index 0000000..d6062c5
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/PositionAndRole/__tests__/index.test.js
@@ -0,0 +1,96 @@
+import {fireEvent, render, screen, waitFor} from "@testing-library/react";
+import React from 'react';
+import {useTranslations} from "next-intl";
+import * as Yup from "yup";
+import {Formik} from "formik";
+import MockAppWithProviders from "../../../../../../../../../../mocks/AppWithProvider";
+import PositionAndRole from "../../PositionAndRole";
+
+const CreateFormMock = ({Component}) => {
+ const t = useTranslations();
+ const validationSchema = Yup.object().shape({
+ position: Yup.string().required(t("ExpertMangement.error_message_position")),
+ roles: Yup.string().required(t("ExpertMangement.error_message_roles"))
+ });
+ return (
+ {
+ }}
+ >
+ {formikProps => ()}
+
+ );
+};
+
+describe("PositionAndRole Component From Expert Management (update)", () => {
+ describe("Rendering", () => {
+ it("Position TextField Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const positionTextField = screen.queryByLabelText('سمت');
+ expect(positionTextField).toBeInTheDocument();
+ });
+ });
+ it("Role Select Box Rendered", async () => {
+ render();
+ const roleSelect = screen.getByTestId('select-box');
+ await waitFor(() => {
+ expect(roleSelect).toBeInTheDocument();
+ });
+ });
+ });
+ describe("Behavioral", () => {
+ it("Position TextField Work Correctly", async () => {
+ render();
+ const positionTextField = screen.queryByLabelText('سمت');
+ fireEvent.change(positionTextField, {target: {value: 'examplePosition'}});
+ await waitFor(() => {
+ expect(positionTextField).toHaveValue('examplePosition');
+ })
+ });
+ it("Role Select Box Work Correctly", async () => {
+ render();
+ const roleSelect = screen.getByTestId("option-opener-role");
+ await waitFor(() => {
+ expect(roleSelect).toBeInTheDocument();
+ });
+ fireEvent.mouseDown(roleSelect);
+ await waitFor(() => {
+ const selectItem = screen.queryByText("ادمین");
+ expect(selectItem).toBeInTheDocument();
+ });
+ });
+ });
+ describe("validation", () => {
+ it('Should See Error When Position Input Is Empty', async () => {
+ render();
+
+ const positionInput = screen.queryByLabelText('سمت');
+
+ fireEvent.change(positionInput, {target: {value: ''}});
+ fireEvent.blur(positionInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("سمت خود را وارد کنید")).toBeInTheDocument()
+ });
+ });
+ it('Should Select An Item With Valid Value of role', async () => {
+ render();
+ const roleInput = await waitFor(() => screen.getByTestId("input-role-id"));
+ const roleSelectOpener = await waitFor(() => screen.getByTestId("option-opener-role"));
+
+ fireEvent.mouseDown(roleSelectOpener);
+ const selectItem = await waitFor(() => screen.queryByText("ادمین"));
+ expect(selectItem).toBeInTheDocument();
+ fireEvent.click(selectItem);
+ await waitFor(() => {
+ expect(roleInput.value).toBe('1')
+ });
+ });
+ })
+});
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/PositionAndRole/index.jsx b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/PositionAndRole/index.jsx
new file mode 100644
index 0000000..4fc402a
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/PositionAndRole/index.jsx
@@ -0,0 +1,75 @@
+import {FormControl, FormHelperText, Grid, InputLabel, MenuItem, Select, TextField} from "@mui/material";
+import {useTranslations} from "next-intl";
+import useRequest from "@/lib/app/hooks/useRequest";
+import useRole from "@/lib/app/hooks/useRole";
+import {log} from "next/dist/server/typescript/utils";
+
+const PositionAndRole = ({formik}) => {
+ const t = useTranslations();
+ const requestServer = useRequest()
+ const {roleList, isLoadingRoleList, errorRoleList} = useRole();
+
+ return (
+
+
+
+
+
+
+ {t("ExpertMangement.text_field_roles")}
+
+
+ {formik.touched.roles && formik.errors.roles ? formik.errors.roles : ""}
+
+
+
+
+ )
+}
+export default PositionAndRole
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/Province/__tests__/index.test.js b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/Province/__tests__/index.test.js
new file mode 100644
index 0000000..d727188
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/Province/__tests__/index.test.js
@@ -0,0 +1,71 @@
+import {fireEvent, render, screen, waitFor} from "@testing-library/react";
+import React from 'react';
+import {useTranslations} from "next-intl";
+import * as Yup from "yup";
+import {Formik} from "formik";
+import Province from "../../Province";
+import MockAppWithProviders from "../../../../../../../../../../mocks/AppWithProvider";
+
+const CreateFormMock = ({Component}) => {
+ const t = useTranslations();
+ const validationSchema = Yup.object().shape({
+ province_id: Yup.string().required(t("ExpertMangement.error_message_province_id")),
+ });
+ return (
+ {
+ }}
+ >
+ {formikProps => ()}
+
+ );
+};
+
+describe("Province Component From Expert Management (update)", () => {
+ describe("Rendering", () => {
+ it("Province Select Box Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const provinceSelectBox = screen.getByTestId('select-box');
+ expect(provinceSelectBox).toBeInTheDocument();
+ });
+ });
+ });
+ describe("Behavioral", () => {
+ it("Province Select Box Work Correctly", async () => {
+ render();
+ const provinceSelectBox = screen.getByTestId('select-box');
+ const provinceSelectOpener = await waitFor(() => screen.getByTestId("option-opener-province"));
+
+ await waitFor(() => {
+ expect(provinceSelectBox).toBeInTheDocument();
+ });
+
+ fireEvent.mouseDown(provinceSelectOpener);
+ await waitFor(() => {
+ const selectItem = screen.queryByText("آذربایجان شرقی");
+ expect(selectItem).toBeInTheDocument();
+ });
+
+ });
+ });
+ describe("validation", () => {
+ it('Should Select An Item With Valid Value of province', async () => {
+ render();
+ const provinceInput = screen.getByTestId("input-province-id")
+ const provinceSelectOpener = await waitFor(() => screen.getByTestId("option-opener-province"));
+
+ fireEvent.mouseDown(provinceSelectOpener);
+ const selectItem = await waitFor(() => screen.queryByText("آذربایجان شرقی"));
+ expect(selectItem).toBeInTheDocument();
+ fireEvent.click(selectItem);
+ await waitFor(() => {
+ expect(provinceInput.value).toBe('1')
+ });
+ });
+ })
+});
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/Province/index.jsx b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/Province/index.jsx
new file mode 100644
index 0000000..763465e
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/Province/index.jsx
@@ -0,0 +1,61 @@
+import {FormControl, FormHelperText, Grid, InputLabel, MenuItem, Select} from "@mui/material";
+import {useTranslations} from "next-intl";
+import useProvince from "@/lib/app/hooks/useProvince";
+import useRequest from "@/lib/app/hooks/useRequest";
+
+const Province = ({formik}) => {
+ const t = useTranslations();
+ const requestServer = useRequest()
+ const {provinceList, isLoadingProvinceList, errorProvinceList} = useProvince();
+
+ return (
+
+
+
+ {t("ExpertMangement.text_field_province_id")}
+
+
+ {formik.touched.province_id && formik.errors.province_id ? formik.errors.province_id : ""}
+
+
+
+
+ )
+}
+export default Province
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/index.jsx b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/index.jsx
new file mode 100644
index 0000000..36c1877
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/index.jsx
@@ -0,0 +1,13 @@
+import Province from "@/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/Province";
+import PositionAndRole
+ from "@/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/RestInfo/PositionAndRole";
+
+const RestInfo = ({formik}) => {
+ return (
+ <>
+
+
+ >
+ )
+}
+export default RestInfo
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/UserInfo/__tests__/index.test.js b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/UserInfo/__tests__/index.test.js
new file mode 100644
index 0000000..3fd341d
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/UserInfo/__tests__/index.test.js
@@ -0,0 +1,62 @@
+import {act, fireEvent, render, screen, waitFor} from "@testing-library/react";
+import React from 'react';
+import MockAppWithProviders from "../../../../../../../../../mocks/AppWithProvider";
+import {useTranslations} from "next-intl";
+import * as Yup from "yup";
+import {Formik} from "formik";
+import UserInfo from "../../UserInfo";
+
+const CreateFormMock = ({Component}) => {
+ const t = useTranslations();
+ const validationSchema = Yup.object().shape({
+ username: Yup.string().required(t("ExpertMangement.error_message_username")),
+ });
+ return (
+ {
+ }}
+ >
+ {formikProps => ()}
+
+ );
+};
+
+describe("UserInfo Component From Expert Management (update)", () => {
+ describe("Rendering", () => {
+ it("UserName TextField Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const usernameTextField = screen.queryByLabelText('نام کاربری');
+ expect(usernameTextField).toBeInTheDocument();
+ });
+ });
+ });
+ describe("Behavioral", () => {
+ it("Username TextField Work Correctly", async () => {
+ render();
+ const usernameTextField = screen.queryByLabelText('نام کاربری');
+ fireEvent.change(usernameTextField, {target: {value: 'exampleUsername'}});
+ await act(() => {
+ expect(usernameTextField).toHaveValue('exampleUsername');
+ })
+ });
+ });
+ describe("validation", () => {
+ it('Should See Error When Username Input Is Empty', async () => {
+ render();
+
+ const usernameInput = screen.queryByLabelText('نام کاربری');
+
+ fireEvent.change(usernameInput, {target: {value: ''}});
+ fireEvent.blur(usernameInput);
+
+ await waitFor(() => {
+ expect(screen.queryByText("نام کاربری خود را وارد کنید")).toBeInTheDocument()
+ });
+ });
+ })
+});
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/UserInfo/index.jsx b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/UserInfo/index.jsx
new file mode 100644
index 0000000..d592ea5
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/UserInfo/index.jsx
@@ -0,0 +1,29 @@
+import {Grid, TextField} from "@mui/material";
+import {useTranslations} from "next-intl";
+
+const UserInfo = ({formik}) => {
+ const t = useTranslations();
+
+ return (
+ <>
+
+
+
+
+
+ >
+ )
+}
+export default UserInfo
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/__tests__/index.test.js b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/__tests__/index.test.js
new file mode 100644
index 0000000..765bc09
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/__tests__/index.test.js
@@ -0,0 +1,87 @@
+import {fireEvent, render, screen, waitFor} from "@testing-library/react";
+import React from 'react';
+import MockAppWithProviders from "../../../../../../../../mocks/AppWithProvider";
+import UpdateContent from "../../UpdateContent";
+
+const row = {
+ original: {
+ avatar: null,
+ created_at: "2023-10-10T13:05:01.000000Z",
+ email: "fisher.sigrid@yahoo.com",
+ full_name: "Anibal Labadie",
+ gender: "female",
+ id: 85,
+ national_id: "5944646138",
+ phone_number: "09731311720",
+ position: "boss1",
+ province_fa: "اردبیل",
+ province_id: 3,
+ roles: [{
+ created_at: "2023-10-10T13:03:18.000000Z",
+ guard_name: "api",
+ id: 2,
+ name: "manager",
+ name_fa: "مدیر"
+ }],
+ telephone_id: "tel-3110",
+ updated_at: "2023-10-17T10:57:12.000000Z",
+ username: "florence.kihn",
+ }
+}
+
+function selectDropdownItem(screen, openerTestId, itemText) {
+ const opener = screen.getByTestId(openerTestId);
+ fireEvent.mouseDown(opener);
+ const selectItem = screen.queryByText(itemText);
+ fireEvent.click(selectItem);
+}
+
+function setInputValue(screen, inputElement, value) {
+ fireEvent.change(inputElement, {target: {value}});
+}
+
+describe("UpdateContent Component From Expert Management (update)", () => {
+ describe("Rendering", () => {
+ it("close button Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const cancelBtn = screen.queryByText('بستن');
+ expect(cancelBtn).toBeInTheDocument();
+ });
+ });
+ it("confirm button Rendered", async () => {
+ render();
+ await waitFor(() => {
+ const confirmBtn = screen.queryByText('ثبت');
+ expect(confirmBtn).toBeInTheDocument();
+ });
+ });
+ });
+ describe("Form Submission", () => {
+ it('Should enable the submit button when the inputs are valid', async () => {
+ render();
+
+ const submitButton = screen.queryByText('ثبت');
+
+ const nameInput = screen.queryByLabelText('نام کامل');
+ const usernameInput = screen.queryByLabelText('نام کاربری');
+ const emailInput = screen.queryByLabelText('پست الکترونیک');
+ const phoneNumberInput = screen.queryByLabelText('شماره همراه');
+ const telephoneIdInput = screen.queryByLabelText('کد تلفن');
+ const nationalIdInput = screen.queryByLabelText('کد ملی');
+ const positionInput = screen.queryByLabelText('سمت');
+
+ setInputValue(screen, nameInput, 'nameTest');
+ setInputValue(screen, usernameInput, 'usernameTest');
+ setInputValue(screen, emailInput, 'emailTest');
+ setInputValue(screen, phoneNumberInput, 'phoneNumberTest');
+ setInputValue(screen, telephoneIdInput, 'telephoneIdTest');
+ setInputValue(screen, nationalIdInput, 'nationalIdTest');
+ setInputValue(screen, positionInput, 'positionTest');
+
+ await waitFor(() => {
+ expect(submitButton).not.toBeDisabled();
+ });
+ });
+ })
+});
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/index.jsx b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/index.jsx
new file mode 100644
index 0000000..1f482dc
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/UpdateForm/UpdateContent/index.jsx
@@ -0,0 +1,108 @@
+import {Box, Button, Chip, DialogActions, DialogContent, Divider,} from "@mui/material";
+import * as Yup from "yup";
+import {useTranslations} from "next-intl";
+import {useFormik} from "formik";
+import useRequest from "@/lib/app/hooks/useRequest";
+import {UPDATE_EXPERT} from "@/core/data/apiRoutes";
+import PersonalInfo from "./PersonalInfo";
+import UserInfo from "./UserInfo";
+import RestInfo from "./RestInfo";
+
+const UpdateContent = ({row, mutate, setOpenUpdateDialog}) => {
+ const t = useTranslations();
+ const requestServer = useRequest()
+
+ const validationSchema = Yup.object().shape({
+ full_name: Yup.string().required(t("ExpertMangement.error_message_full_name")),
+ username: Yup.string().required(t("ExpertMangement.error_message_username")),
+ email: Yup.string().required(t("ExpertMangement.error_message_email")),
+ phone_number: Yup.string().required(t("ExpertMangement.error_message_phone_number")),
+ telephone_id: Yup.string().required(t("ExpertMangement.error_message_telephone_id")),
+ gender: Yup.string().required(t("ExpertMangement.error_message_gender")),
+ national_id: Yup.string().required(t("ExpertMangement.error_message_national_id")),
+ position: Yup.string().required(t("ExpertMangement.error_message_position")),
+ province_id: Yup.string().required(t("ExpertMangement.error_message_province_id")),
+ roles: Yup.string().required(t("ExpertMangement.error_message_roles"))
+ });
+
+ const formik = useFormik({
+ initialValues: {
+ full_name: row.original.full_name,
+ username: row.original.username,
+ email: row.original.email,
+ phone_number: row.original.phone_number,
+ telephone_id: row.original.telephone_id,
+ gender: row.original.gender,
+ national_id: row.original.national_id,
+ position: row.original.position,
+ province_id: row.original.province_id,
+ roles: row.original.roles[0]?.id
+ },
+ validationSchema,
+ onSubmit: (values, {setSubmitting}) => {
+ const formData = new FormData();
+ formData.append("full_name", values.full_name);
+ formData.append("national_id", values.national_id);
+ formData.append("phone_number", values.phone_number);
+ formData.append("telephone_id", values.telephone_id);
+ formData.append("gender", values.gender);
+ formData.append("email", values.email);
+ formData.append("username", values.username);
+ formData.append("province_id", values.province_id);
+ formData.append("position", values.position);
+ formData.append("roles", values.roles);
+
+ requestServer(`${UPDATE_EXPERT}/${row.original.id}`, 'post', {auth: true, data: formData})
+ .then((response) => {
+ setOpenUpdateDialog(false)
+ mutate()
+ }).catch(() => {
+ }).finally(() => {
+ setSubmitting(false);
+ });
+ },
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default UpdateContent
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/UpdateForm/__tests__/index.test.js b/src/components/dashboard/expert-management/Form/UpdateForm/__tests__/index.test.js
new file mode 100644
index 0000000..f824d6d
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/UpdateForm/__tests__/index.test.js
@@ -0,0 +1,147 @@
+import {act, fireEvent, render, screen, waitFor} from "@testing-library/react";
+import React from 'react';
+import MockAppWithProviders from "../../../../../../../mocks/AppWithProvider";
+import UpdateForm from "../../UpdateForm";
+
+const row = {
+ original: {
+ avatar: null,
+ created_at: "2023-10-10T13:05:01.000000Z",
+ email: "fisher.sigrid@yahoo.com",
+ full_name: "Anibal Labadie",
+ gender: "female",
+ id: 85,
+ national_id: "5944646138",
+ phone_number: "09731311720",
+ position: "boss1",
+ province_fa: "اردبیل",
+ province_id: 3,
+ roles: [{
+ created_at: "2023-10-10T13:03:18.000000Z",
+ guard_name: "api",
+ id: 2,
+ name: "manager",
+ name_fa: "مدیر"
+ }],
+ telephone_id: "tel-3110",
+ updated_at: "2023-10-17T10:57:12.000000Z",
+ username: "florence.kihn",
+ }
+}
+
+function selectDropdownItem(screen, openerTestId, itemText) {
+ const opener = screen.getByTestId(openerTestId);
+ fireEvent.mouseDown(opener);
+ const selectItem = screen.queryByText(itemText);
+ fireEvent.click(selectItem);
+}
+
+function setInputValue(screen, inputElement, value) {
+ fireEvent.change(inputElement, {target: {value}});
+}
+
+describe("CreateForm Component From Expert Management", () => {
+ describe("Rendering", () => {
+ it("Update Expert Button Rendered", () => {
+ render();
+ const UpdateButton = screen.getByTestId('update-button');
+ expect(UpdateButton).toBeInTheDocument();
+ });
+ });
+ describe("Behavioral", () => {
+ it("by Clicking Update Button Dialog Should Append To Document", async () => {
+ render();
+ const UpdateButton = screen.getByTestId('update-button');
+ fireEvent.click(UpdateButton);
+ await act(() => {
+ const UpdateDialog = screen.getByRole('dialog', {name: "ویرایش کارشناس"})
+ expect(UpdateDialog).toBeInTheDocument();
+ });
+ });
+ });
+ describe("Form Submission", () => {
+ it('Should request to api and if get success close dialog', async () => {
+ render();
+
+ const UpdateButton = screen.getByTestId('update-button');
+ fireEvent.click(UpdateButton);
+
+ const submitButton = screen.queryByText('ثبت');
+
+ const nameInput = screen.queryByLabelText('نام کامل');
+ const usernameInput = screen.queryByLabelText('نام کاربری');
+ const emailInput = screen.queryByLabelText('پست الکترونیک');
+ const phoneNumberInput = screen.queryByLabelText('شماره همراه');
+ const telephoneIdInput = screen.queryByLabelText('کد تلفن');
+ const nationalIdInput = screen.queryByLabelText('کد ملی');
+ const positionInput = screen.queryByLabelText('سمت');
+
+ selectDropdownItem(screen, "option-opener-province", "آذربایجان شرقی");
+ selectDropdownItem(screen, "option-opener-role", "ادمین");
+ selectDropdownItem(screen, "option-opener-gender", "مرد");
+
+ setInputValue(screen, nameInput, 'nameTest');
+ setInputValue(screen, usernameInput, 'usernameTest');
+ setInputValue(screen, emailInput, 'email@gmail.com');
+ setInputValue(screen, phoneNumberInput, '0914577458');
+ setInputValue(screen, telephoneIdInput, '091');
+ setInputValue(screen, nationalIdInput, 'nationalIdTest');
+ setInputValue(screen, positionInput, 'positionTest');
+
+ expect(submitButton).not.toBeDisabled();
+ fireEvent.click(submitButton);
+
+ await waitFor(() => {
+ const dialogContent = screen.queryByTestId('update-dialog-content');
+ expect(dialogContent).not.toBeInTheDocument();
+ });
+ });
+ it('Should request to api and if get error keep previous data', async () => {
+ render();
+
+ const UpdateButton = screen.getByTestId('update-button');
+ fireEvent.click(UpdateButton);
+
+ const submitButton = screen.queryByText('ثبت');
+
+ const nameInput = screen.queryByLabelText('نام کامل');
+ const usernameInput = screen.queryByLabelText('نام کاربری');
+ const emailInput = screen.queryByLabelText('پست الکترونیک');
+ const phoneNumberInput = screen.queryByLabelText('شماره همراه');
+ const telephoneIdInput = screen.queryByLabelText('کد تلفن');
+ const nationalIdInput = screen.queryByLabelText('کد ملی');
+ const positionInput = screen.queryByLabelText('سمت');
+ const provinceInput = screen.getByTestId("input-province-id")
+ const roleInput = screen.getByTestId("input-role-id")
+ const genderInput = screen.getByTestId("input-gender")
+
+ selectDropdownItem(screen, "option-opener-province", "آذربایجان شرقی");
+ selectDropdownItem(screen, "option-opener-role", "ادمین");
+ selectDropdownItem(screen, "option-opener-gender", "مرد");
+
+ setInputValue(screen, nameInput, 'nameTest');
+ setInputValue(screen, usernameInput, 'usernameTest');
+ setInputValue(screen, emailInput, 'emailTest@gmail.com');
+ setInputValue(screen, phoneNumberInput, '0914577458');
+ setInputValue(screen, telephoneIdInput, '091');
+ setInputValue(screen, nationalIdInput, 'nationalIdTest');
+ setInputValue(screen, positionInput, 'positionTest');
+
+ expect(submitButton).not.toBeDisabled();
+ await fireEvent.click(submitButton);
+
+ await waitFor(() => {
+ expect(nameInput).toHaveValue('nameTest');
+ expect(usernameInput).toHaveValue('usernameTest');
+ expect(emailInput).toHaveValue('emailTest@gmail.com');
+ expect(phoneNumberInput).toHaveValue('0914577458');
+ expect(telephoneIdInput).toHaveValue('091');
+ expect(nationalIdInput).toHaveValue('nationalIdTest');
+ expect(positionInput).toHaveValue('positionTest');
+ expect(provinceInput).toHaveValue('1');
+ expect(roleInput).toHaveValue('1');
+ expect(genderInput).toHaveValue('male');
+ });
+ });
+ })
+});
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/Form/UpdateForm/index.jsx b/src/components/dashboard/expert-management/Form/UpdateForm/index.jsx
new file mode 100644
index 0000000..27fae5c
--- /dev/null
+++ b/src/components/dashboard/expert-management/Form/UpdateForm/index.jsx
@@ -0,0 +1,33 @@
+import {useTranslations} from "next-intl";
+import {Dialog, DialogTitle, IconButton, Tooltip} from "@mui/material";
+import EditIcon from '@mui/icons-material/Edit';
+import {useState} from "react";
+import UpdateContent from "./UpdateContent";
+
+
+const Update = ({row, mutate}) => {
+ const t = useTranslations();
+ const [openUpdateDialog, setOpenUpdateDialog] = useState(false);
+
+ return (
+ <>
+
+ {
+ setOpenUpdateDialog(true)
+ }}
+ >
+
+
+
+
+ >
+ );
+};
+export default Update;
\ No newline at end of file
diff --git a/src/components/dashboard/expert-management/TableRowActions.jsx b/src/components/dashboard/expert-management/TableRowActions.jsx
new file mode 100644
index 0000000..91e778e
--- /dev/null
+++ b/src/components/dashboard/expert-management/TableRowActions.jsx
@@ -0,0 +1,24 @@
+import {Box} from "@mui/material";
+import Update from "./Form/UpdateForm"
+import Delete from "./Form/DeleteForm";
+
+const TableRowActions = ({row, mutate}) => {
+ return (
+
+
+ {/**/}
+
+
+ );
+};
+
+export default TableRowActions;
diff --git a/src/components/dashboard/expert-management/TableToolbar.jsx b/src/components/dashboard/expert-management/TableToolbar.jsx
new file mode 100644
index 0000000..24f26f7
--- /dev/null
+++ b/src/components/dashboard/expert-management/TableToolbar.jsx
@@ -0,0 +1,14 @@
+import {useTranslations} from "next-intl";
+import Create from "./Form/CreateForm";
+import {Box} from "@mui/material";
+
+function TableToolbar({mutate}) {
+ const t = useTranslations();
+ return (
+
+
+
+ );
+}
+
+export default TableToolbar;
diff --git a/src/components/dashboard/expert-management/index.jsx b/src/components/dashboard/expert-management/index.jsx
new file mode 100644
index 0000000..632d89b
--- /dev/null
+++ b/src/components/dashboard/expert-management/index.jsx
@@ -0,0 +1,12 @@
+import DashboardLayouts from "@/layouts/DashboardLayout";
+import ExpertManagementDataTable from "@/components/dashboard/expert-management/DataTable";
+
+function DashboardExpertManagementComponent() {
+ return (
+
+
+
+ );
+}
+
+export default DashboardExpertManagementComponent;
diff --git a/src/components/dashboard/first/index.jsx b/src/components/dashboard/first/index.jsx
new file mode 100644
index 0000000..841d677
--- /dev/null
+++ b/src/components/dashboard/first/index.jsx
@@ -0,0 +1,7 @@
+import DashboardLayout from "@/layouts/DashboardLayout";
+
+const DashboardFirstComponent = () => {
+ return ;
+};
+
+export default DashboardFirstComponent;
diff --git a/src/components/dashboard/role-management/Form/CreateForm/CreateContent.jsx b/src/components/dashboard/role-management/Form/CreateForm/CreateContent.jsx
new file mode 100644
index 0000000..d06659b
--- /dev/null
+++ b/src/components/dashboard/role-management/Form/CreateForm/CreateContent.jsx
@@ -0,0 +1,153 @@
+import {
+ Button,
+ Checkbox,
+ CircularProgress,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ FormControl,
+ FormControlLabel,
+ FormHelperText,
+ FormLabel,
+ Grid,
+ Stack,
+ TextField,
+ Typography
+} from "@mui/material";
+import {useTranslations} from "next-intl";
+import useRequest from "@/lib/app/hooks/useRequest";
+import * as Yup from "yup";
+import {useFormik} from "formik";
+import {ADD_ROLE} from "@/core/data/apiRoutes";
+import usePermissions from "@/lib/app/hooks/usePermissions";
+
+const CreateContent = ({mutate, setOpenConfirmDialog}) => {
+ const t = useTranslations();
+ const requestServer = useRequest({auth: true})
+ const {permissions_list, isLoading} = usePermissions()
+ const validationSchema = Yup.object().shape({
+ name_en: Yup.string().required(t("AddDialog.name_en_error")),
+ name_fa: Yup.string().required(t("AddDialog.name_fa_error")),
+ permissions: Yup.array().min(1, t("AddDialog.permission_min_error")).required(t("AddDialog.permission")),
+ });
+ const formik = useFormik({
+ initialValues: {
+ name_en: "",
+ name_fa: "",
+ permissions: [],
+ }, validationSchema, onSubmit: (values, {setSubmitting}) => {
+ const formData = new FormData();
+ formData.append("name", values.name_en);
+ formData.append("name_fa", values.name_fa);
+ for (let i = 0; i < values.permissions.length; i++) {
+ formData.append(`permissions[${i}]`, values.permissions[i]);
+ }
+
+ requestServer(ADD_ROLE, 'post', {
+ data: formData,
+ }).then(() => {
+ setOpenConfirmDialog(false)
+ mutate()
+ update_notification()
+ }).catch(() => {
+ }).finally(() => {
+ setSubmitting(false);
+ });
+ },
+ });
+
+ return (
+ <>
+ {t("AddDialog.add")}
+
+
+
+
+
+
+
+ {t("AddDialog.permission")}
+
+ {formik.touched.permissions && formik.errors.permissions}
+ {isLoading ?
+
+
+ {t("AddDialog.loading_permissions_list")}
+
+ : (
+
+ <>
+ {permissions_list.map((permission) => (
+
+ {
+ if (e.target.checked) {
+ formik.setFieldValue("permissions", [...formik.values.permissions, permission.id])
+ } else {
+ formik.setFieldValue("permissions", formik.values.permissions.filter((id) => id !== permission.id))
+ }
+ }}
+ />
+ }
+ label={permission.name_fa}
+ />
+
+ ))}
+ >
+
+ )
+ }
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+export default CreateContent
\ No newline at end of file
diff --git a/src/components/dashboard/role-management/Form/CreateForm/__test__/CreateContent.test.js b/src/components/dashboard/role-management/Form/CreateForm/__test__/CreateContent.test.js
new file mode 100644
index 0000000..610e224
--- /dev/null
+++ b/src/components/dashboard/role-management/Form/CreateForm/__test__/CreateContent.test.js
@@ -0,0 +1,167 @@
+import {act, fireEvent, render, screen, waitFor} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../../mocks/AppWithProvider";
+import CreateContent from "@/components/dashboard/role-management/Form/CreateForm/CreateContent";
+
+describe("Create Content component from Create Form Component in Role Management Component",()=>{
+ describe("Rendering", ()=>{
+ it('should see AddDialog text in the top ', async () => {
+ render(
+
+
+
+ )
+ const textElement = screen.queryByText("افزودن")
+ await waitFor(()=>{
+ expect(textElement).toBeInTheDocument()
+ })
+ });
+ it('should see name_en text', async () => {
+ render(
+
+
+
+ )
+ const textElement = screen.queryByLabelText("نام انگلیسی")
+ await waitFor(()=>{
+ expect(textElement).toBeInTheDocument()
+ })
+ });
+ it('should see name_fa text', async () => {
+ render(
+
+
+
+ )
+ const textElement = screen.queryByLabelText("نام فارسی")
+ await waitFor(()=>{
+ expect(textElement).toBeInTheDocument()
+ })
+ });
+ it('should see name_fa text', async () => {
+ render(
+
+
+
+ )
+ const textElement = screen.queryByLabelText("نام فارسی")
+ await waitFor(()=>{
+ expect(textElement).toBeInTheDocument()
+ })
+ });
+ })
+ describe("Behavior",()=>{
+ it('should see what fill in the name_en input', async () => {
+ render(
+
+
+
+ )
+ const nameEnglishInput = screen.getByLabelText('نام انگلیسی');
+
+ fireEvent.change(nameEnglishInput, {target: {value: 'testuser'}});
+ await act(()=>{
+ // Simulate user input
+ expect(nameEnglishInput).toHaveValue('testuser');
+ })
+ });
+ it('should see what fill in the name_fa input', async () => {
+ render(
+
+
+
+ )
+ const nameFarsiInput = screen.getByLabelText('نام فارسی');
+
+ fireEvent.change(nameFarsiInput, {target: {value: 'testuser'}});
+ await act(()=>{
+ // Simulate user input
+ expect(nameFarsiInput).toHaveValue('testuser');
+ })
+ });
+ it('should return permissions_list', async () => {
+ render(
+
+
+
+ )
+ const checkboxElement = await screen.findAllByTestId("PermissionList-checkbox")
+ expect(checkboxElement).toHaveLength(2)
+ });
+ it('should get checked if permission list item get clicked', async () => {
+ render(
+
+
+
+ )
+ const checkboxElement = screen.getByLabelText("مدیریت کارتابل رییس اداره مسافری استان")
+
+ await waitFor(()=>{
+ expect(checkboxElement).not.toBeChecked();
+ })
+
+ fireEvent.click(checkboxElement);
+
+ await waitFor(()=>{
+ expect(checkboxElement).toBeChecked();
+ })
+ });
+ })
+ describe("Validation",()=>{
+ it('should see error text when name_fa input is empty', async () => {
+ render(
+
+
+
+ )
+ const name_faInput = screen.getByLabelText("نام فارسی")
+ expect(screen.queryByText("وارد کردن نام فارسی الزامیست")).not.toBeInTheDocument()
+ fireEvent.blur(name_faInput);
+
+ await waitFor(()=>{
+ expect(screen.queryByText("وارد کردن نام فارسی الزامیست")).toBeInTheDocument()
+ })
+ fireEvent.change(name_faInput, {target : {value : "نام فارسی"}})
+
+ await waitFor(()=>{
+ expect(screen.queryByText("وارد کردن نام فارسی الزامیست")).not.toBeInTheDocument()
+ })
+ })
+ it('should see error text when name_en input is empty', async () => {
+ render(
+
+
+
+ )
+ const name_enInput = screen.getByLabelText("نام انگلیسی")
+ expect(screen.queryByText("وارد کردن نام انگلیسی الزامیست")).not.toBeInTheDocument()
+ fireEvent.blur(name_enInput);
+
+ await waitFor(()=>{
+ expect(screen.queryByText("وارد کردن نام انگلیسی الزامیست")).toBeInTheDocument()
+ })
+ fireEvent.change(name_enInput, {target : {value : "english name"}})
+
+ await waitFor(()=>{
+ expect(screen.queryByText("وارد کردن نام انگلیسی الزامیست")).not.toBeInTheDocument()
+ })
+ })
+ it('should see errors when click on submit button and field not completed', async () => {
+ render(
+
+
+
+ )
+ const submitButtonElement = screen.queryByText("ثبت")
+ fireEvent.click(submitButtonElement)
+ await waitFor(()=>{
+ expect(screen.queryByText("وارد کردن نام انگلیسی الزامیست")).toBeInTheDocument()
+ })
+ await waitFor(()=>{
+ expect(screen.queryByText("وارد کردن نام فارسی الزامیست")).toBeInTheDocument()
+ })
+ await waitFor(()=>{
+ expect(screen.queryByText("حداقل باید یک دسترسی انتخاب شود")).toBeInTheDocument()
+ })
+ });
+ })
+})
\ No newline at end of file
diff --git a/src/components/dashboard/role-management/Form/CreateForm/__test__/index.test.js b/src/components/dashboard/role-management/Form/CreateForm/__test__/index.test.js
new file mode 100644
index 0000000..7bc9db2
--- /dev/null
+++ b/src/components/dashboard/role-management/Form/CreateForm/__test__/index.test.js
@@ -0,0 +1,34 @@
+import {fireEvent, queryByText, render, screen, waitFor} from "@testing-library/react";
+import CreateForm from "@/components/dashboard/role-management/Form/CreateForm";
+import MockAppWithProviders from "../../../../../../../mocks/AppWithProvider";
+
+describe("Create Form Component from Role Management Component",()=>{
+ describe("Rendering",()=>{
+
+ it('should see Dialog text', async () => {
+ render(
+
+
+
+ )
+ const textElement = screen.queryByText("افزودن")
+ await waitFor(()=>{
+ expect(textElement).toBeInTheDocument()
+ })
+ });
+
+ it('should see Tooltip text when mouse over', async () => {
+ render(
+
+
+
+ )
+ const buttonElement = screen.getByText("افزودن");
+ fireEvent.mouseOver(buttonElement);
+ const tooltipTextElement = screen.getByText("افزودن");
+ await waitFor(()=>{
+ expect(tooltipTextElement).toBeInTheDocument()
+ })
+ });
+ })
+})
\ No newline at end of file
diff --git a/src/components/dashboard/role-management/Form/CreateForm/index.jsx b/src/components/dashboard/role-management/Form/CreateForm/index.jsx
new file mode 100644
index 0000000..455d9a2
--- /dev/null
+++ b/src/components/dashboard/role-management/Form/CreateForm/index.jsx
@@ -0,0 +1,34 @@
+import {Button, Dialog, Stack, Tooltip} from "@mui/material";
+import DataSaverOnIcon from "@mui/icons-material/DataSaverOn";
+import {useTranslations} from "next-intl";
+import {useState} from "react";
+import CreateContent from "./CreateContent";
+
+const CreateForm = ({mutate}) => {
+ const t = useTranslations();
+ const [openConfirmDialog, setOpenConfirmDialog] = useState(false);
+
+ return (
+
+
+
+
+
+
+ )
+}
+export default CreateForm
\ No newline at end of file
diff --git a/src/components/dashboard/role-management/Form/DeleteForm/DeleteContent.jsx b/src/components/dashboard/role-management/Form/DeleteForm/DeleteContent.jsx
new file mode 100644
index 0000000..f1580ce
--- /dev/null
+++ b/src/components/dashboard/role-management/Form/DeleteForm/DeleteContent.jsx
@@ -0,0 +1,42 @@
+import {Button, DialogActions, DialogContent, DialogTitle, Typography} from "@mui/material";
+import {DELETE_ROLE} from "@/core/data/apiRoutes";
+import {useState} from "react";
+import useRequest from "@/lib/app/hooks/useRequest";
+import useNotification from "@/lib/app/hooks/useNotification";
+import {useTranslations} from "next-intl";
+
+const DeleteContent = ({rowId, mutate, setOpenConfirmDialog}) => {
+ const t = useTranslations();
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const requestServer = useRequest({auth: true})
+ const {update_notification} = useNotification()
+ const handleSubmit = () => {
+ setIsSubmitting(true)
+ requestServer(`${DELETE_ROLE}/${rowId}`, 'delete').then((response) => {
+ mutate()
+ update_notification()
+ }).catch(() => {
+ }).finally(() => {
+ setIsSubmitting(false)
+ });
+ }
+ return (
+ <>
+ {t("DeleteDialog.delete")}
+
+ {t("DeleteDialog.typography")}
+
+
+
+
+
+ >
+ )
+}
+export default DeleteContent
\ No newline at end of file
diff --git a/src/components/dashboard/role-management/Form/DeleteForm/__test__/DeleteContent.test.js b/src/components/dashboard/role-management/Form/DeleteForm/__test__/DeleteContent.test.js
new file mode 100644
index 0000000..7258e10
--- /dev/null
+++ b/src/components/dashboard/role-management/Form/DeleteForm/__test__/DeleteContent.test.js
@@ -0,0 +1,53 @@
+import {act, render, screen, waitFor} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../../mocks/AppWithProvider";
+import DeleteContent from "@/components/dashboard/role-management/Form/DeleteForm/DeleteContent";
+import UpdateContent from "@/components/dashboard/role-management/Form/UpdateForm/UpdateContent";
+
+describe("Create Content component from Create Form Component in Role Management Component", () => {
+ describe("Rendering", () => {
+ it('should see DeleteDialog text in the top ', async () => {
+ render(
+
+
+
+ )
+ const textElement = screen.queryByText("حذف")
+ await waitFor(() => {
+ expect(textElement).toBeInTheDocument()
+ })
+ })
+ it('should see DeleteDialog text content ', async () => {
+ render(
+
+
+
+ )
+ const textElement = screen.queryByText("آیا از حدف این مورد اطمینان دارید ؟")
+ await waitFor(() => {
+ expect(textElement).toBeInTheDocument()
+ })
+ })
+ it('should see delete text in the delete button ', async () => {
+ render(
+
+
+
+ )
+ const buttonElement = screen.queryByText("حذف کردن")
+ await waitFor(() => {
+ expect(buttonElement).toBeInTheDocument()
+ })
+ });
+ it('should see cancel text in the cancel button ', async () => {
+ render(
+
+
+
+ )
+ const buttonElement = screen.queryByText("انصراف")
+ await waitFor(() => {
+ expect(buttonElement).toBeInTheDocument()
+ })
+ });
+ })
+})
\ No newline at end of file
diff --git a/src/components/dashboard/role-management/Form/DeleteForm/__test__/index.test.js b/src/components/dashboard/role-management/Form/DeleteForm/__test__/index.test.js
new file mode 100644
index 0000000..d8eaaa2
--- /dev/null
+++ b/src/components/dashboard/role-management/Form/DeleteForm/__test__/index.test.js
@@ -0,0 +1,21 @@
+import {fireEvent, render, screen, waitFor} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../../mocks/AppWithProvider";
+import DeleteForm from "@/components/dashboard/role-management/Form/DeleteForm";
+import UpdateForm from "@/components/dashboard/role-management/Form/UpdateForm";
+
+describe("Create Form Component from Role Management Component",()=> {
+ describe("Rendering", () => {
+ it('should see Dialog text when mouse is over', async () => {
+ render(
+
+
+
+ )
+ const textElement = screen.getByTestId("dialog_tooltip")
+ fireEvent.mouseOver(textElement)
+ await waitFor(()=>{
+ expect(screen.getByText("حذف")).toBeInTheDocument();
+ })
+ });
+ })
+})
\ No newline at end of file
diff --git a/src/components/dashboard/role-management/Form/DeleteForm/index.jsx b/src/components/dashboard/role-management/Form/DeleteForm/index.jsx
new file mode 100644
index 0000000..4ff70bd
--- /dev/null
+++ b/src/components/dashboard/role-management/Form/DeleteForm/index.jsx
@@ -0,0 +1,32 @@
+import {useTranslations} from "next-intl";
+import {useState} from "react";
+import {Dialog, IconButton, Tooltip} from "@mui/material";
+import DeleteIcon from '@mui/icons-material/Delete';
+import DeleteContent from "@/components/dashboard/role-management/Form/DeleteForm/DeleteContent";
+
+const DeleteForm = ({rowId, mutate}) => {
+ const t = useTranslations();
+ const [openConfirmDialog, setOpenConfirmDialog] = useState(false);
+
+
+ return (
+ <>
+
+ {
+ setOpenConfirmDialog(true)
+ }}
+ >
+
+
+
+
+ >
+ );
+};
+export default DeleteForm;
\ No newline at end of file
diff --git a/src/components/dashboard/role-management/Form/UpdateForm/UpdateContent.jsx b/src/components/dashboard/role-management/Form/UpdateForm/UpdateContent.jsx
new file mode 100644
index 0000000..3fee10a
--- /dev/null
+++ b/src/components/dashboard/role-management/Form/UpdateForm/UpdateContent.jsx
@@ -0,0 +1,153 @@
+import {
+ Button,
+ Checkbox,
+ CircularProgress,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ FormControl,
+ FormControlLabel,
+ FormHelperText,
+ FormLabel,
+ Grid,
+ Stack,
+ TextField,
+ Typography
+} from "@mui/material";
+import {useFormik} from "formik";
+import {UPDATE_ROLE} from "@/core/data/apiRoutes";
+import * as Yup from "yup";
+import usePermissions from "@/lib/app/hooks/usePermissions";
+import useNotification from "@/lib/app/hooks/useNotification";
+import useRequest from "@/lib/app/hooks/useRequest";
+import {useTranslations} from "next-intl";
+
+const UpdateContent = ({mutate, row, setOpenConfirmDialog}) => {
+ const t = useTranslations();
+ const requestServer = useRequest({auth: true})
+ const {update_notification} = useNotification()
+ const {permissions_list, isLoading} = usePermissions()
+
+ const validationSchema = Yup.object().shape({
+ name_en: Yup.string().required(t("UpdateDialog.name_en_error")),
+ name_fa: Yup.string().required(t("UpdateDialog.name_fa_error")),
+ permissions: Yup.array().min(1, t("UpdateDialog.permission_min_error")).required(t("UpdateDialog.permission")),
+ });
+
+ const formik = useFormik({
+ initialValues: {
+ name_en: row.getValue("name"),
+ name_fa: row.getValue("name_fa"),
+ permissions: row.original.permissions.map((obj) => obj.id),
+ }, validationSchema, onSubmit: (values, {setSubmitting}) => {
+ const formData = new FormData();
+ formData.append("name", values.name_en);
+ formData.append("name_fa", values.name_fa);
+ for (let i = 0; i < values.permissions.length; i++) {
+ formData.append(`permissions[${i}]`, values.permissions[i]);
+ }
+
+ requestServer(`${UPDATE_ROLE}/${row.getValue("id")}`, 'post', {
+ data: formData,
+ }).then((response) => {
+ setOpenConfirmDialog(false)
+ mutate()
+ update_notification()
+ }).catch(() => {
+ }).finally(() => {
+ setSubmitting(false);
+ });
+ },
+ });
+
+ return (
+ <>
+ {t("UpdateDialog.update")}
+
+
+
+
+
+
+
+ {t("UpdateDialog.permission")}
+
+ {formik.touched.permissions && formik.errors.permissions}
+ {isLoading ?
+
+
+ {t("UpdateDialog.loading_permissions_list")}
+
+ : (
+
+ <>
+ {permissions_list.map((permission) => (
+
+ {
+ if (e.target.checked) {
+ formik.setFieldValue("permissions", [...formik.values.permissions, permission.id])
+ } else {
+ formik.setFieldValue("permissions", formik.values.permissions.filter((id) => id !== permission.id))
+ }
+ }}
+ />
+ }
+ label={permission.name_fa}
+ />
+
+ ))}
+ >
+
+ )}
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+export default UpdateContent
\ No newline at end of file
diff --git a/src/components/dashboard/role-management/Form/UpdateForm/__test__/UpdateContent.test.js b/src/components/dashboard/role-management/Form/UpdateForm/__test__/UpdateContent.test.js
new file mode 100644
index 0000000..caaa470
--- /dev/null
+++ b/src/components/dashboard/role-management/Form/UpdateForm/__test__/UpdateContent.test.js
@@ -0,0 +1,209 @@
+import {act, fireEvent, render, screen, waitFor} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../../mocks/AppWithProvider";
+import UpdateContent from "@/components/dashboard/role-management/Form/UpdateForm/UpdateContent";
+const row = {
+ id : 0,
+ getValue : name => {
+ if (name === "name") {
+ return "manage_passenger_office_navgan";
+ }
+ else if (name === "name_fa") {
+ return "مدیریت کارتابل رییس اداره مسافری استان";
+ }
+ },
+ original : {
+ permissions : [
+ {
+ id: 1,
+ name: "manage_passenger_office_navgan",
+ name_fa: "مدیریت کارتابل رییس اداره مسافری استان"
+ },
+ {
+ id: 2,
+ name: "manage_passenger_office_navgan",
+ name_fa: "مدیریت کارتابل رییس اداره مسافری استان"
+ }
+ ]
+ }
+}
+
+describe("Create Content component from Create Form Component in Role Management Component",()=>{
+ describe("Rendering", ()=>{
+ it('should see AddDialog text in the top ', async () => {
+ render(
+
+
+
+ )
+ const textElement = screen.queryByText("ویرایش")
+ await waitFor(()=>{
+ expect(textElement).toBeInTheDocument()
+ })
+ });
+ it('should see name_en text', async () => {
+ render(
+
+
+
+ )
+ const textElement = screen.queryByLabelText("نام انگلیسی")
+ await waitFor(()=>{
+ expect(textElement).toBeInTheDocument()
+ })
+ });
+ it('should see name_fa text', async () => {
+ render(
+
+
+
+ )
+ const textElement = screen.queryByLabelText("نام فارسی")
+ await waitFor(()=>{
+ expect(textElement).toBeInTheDocument()
+ })
+ });
+ it('should see update text in the submit button ', async () => {
+ render(
+
+
+
+ )
+ const buttonElement = screen.queryByText("ثبت")
+ await waitFor(()=>{
+ expect(buttonElement).toBeInTheDocument()
+ })
+ });
+ it('should see cancel text in the cancel button ', async () => {
+ render(
+
+
+
+ )
+ const buttonElement = screen.queryByText("بستن")
+ await waitFor(()=>{
+ expect(buttonElement).toBeInTheDocument()
+ })
+ });
+ })
+
+ describe("Behavior", ()=>{
+ it('should see what fill in the name_en input', async () => {
+ render(
+
+
+
+ )
+ const nameEnglishInput = screen.getByLabelText('نام انگلیسی');
+
+ fireEvent.change(nameEnglishInput, {target: {value: 'testuser'}});
+ await waitFor(()=>{
+ // Simulate user input
+ expect(nameEnglishInput).toHaveValue('testuser');
+ })
+ });
+ it('should see what fill in the name_fa input', async () => {
+ render(
+
+
+
+ )
+ const nameFarsiInput = screen.getByLabelText('نام فارسی');
+
+ fireEvent.change(nameFarsiInput, {target: {value: 'testuser'}});
+ await waitFor(()=>{
+ // Simulate user input
+ expect(nameFarsiInput).toHaveValue('testuser');
+ })
+ });
+ it('should return permissions_list', async () => {
+ render(
+
+
+
+ )
+ const checkboxElement = await screen.findAllByTestId("PermissionList-checkbox")
+ await waitFor(()=>{
+ expect(checkboxElement).toHaveLength(2)
+ })
+ });
+ it('should see the value of the name_en that pass to input value', async () => {
+ render(
+
+
+
+ )
+ const name_enElement = screen.getByLabelText("نام انگلیسی")
+ await waitFor(()=>{
+ expect(name_enElement).toHaveValue("manage_passenger_office_navgan")
+ })
+ });
+ it('should see the value of the name_fa that pass to input value', async () => {
+ render(
+
+
+
+ )
+ const name_enElement = screen.getByLabelText("نام فارسی")
+ await waitFor(()=>{
+ expect(name_enElement).toHaveValue("مدیریت کارتابل رییس اداره مسافری استان")
+ })
+ });
+ it('should get checked if the id exists in permission lists', async () => {
+ render(
+
+
+
+ )
+ const checkboxElement = screen.getByLabelText("مدیریت کارتابل رییس اداره مسافری استان")
+ await waitFor(()=>{
+ expect(checkboxElement).toBeChecked();
+ })
+
+ });
+ })
+ describe("Validation", ()=>{
+ it('should see error text when name_fa input is empty', async () => {
+ render(
+
+
+
+ )
+ const name_faInput = screen.getByLabelText("نام فارسی")
+ expect(screen.queryByText("وارد کردن نام فارسی الزامیست")).not.toBeInTheDocument()
+ fireEvent.change(name_faInput, {target : {value : ''}});
+
+ fireEvent.blur(name_faInput)
+
+ await waitFor(()=>{
+ expect(screen.queryByText("وارد کردن نام فارسی الزامیست")).toBeInTheDocument()
+ })
+ fireEvent.change(name_faInput, {target : {value : "نام فارسی"}})
+
+ await waitFor(()=>{
+ expect(screen.queryByText("وارد کردن نام فارسی الزامیست")).not.toBeInTheDocument()
+ })
+ })
+ it('should see error text when name_en input is empty', async () => {
+ render(
+
+
+
+ )
+ const name_enInput = screen.getByLabelText("نام انگلیسی")
+ expect(screen.queryByText("وارد کردن نام انگلیسی الزامیست")).not.toBeInTheDocument()
+
+ fireEvent.change(name_enInput, {target : {value : null}});
+ fireEvent.blur(name_enInput)
+
+ await waitFor(()=>{
+
+ expect(screen.queryByText("وارد کردن نام انگلیسی الزامیست")).toBeInTheDocument()
+ })
+ fireEvent.change(name_enInput, {target : {value : "english name"}})
+
+ await waitFor(()=>{
+ expect(screen.queryByText("وارد کردن نام انگلیسی الزامیست")).not.toBeInTheDocument()
+ })
+ })
+ })
+})
\ No newline at end of file
diff --git a/src/components/dashboard/role-management/Form/UpdateForm/__test__/index.test.js b/src/components/dashboard/role-management/Form/UpdateForm/__test__/index.test.js
new file mode 100644
index 0000000..2a0ee82
--- /dev/null
+++ b/src/components/dashboard/role-management/Form/UpdateForm/__test__/index.test.js
@@ -0,0 +1,22 @@
+import {fireEvent, render, screen, waitFor} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../../mocks/AppWithProvider";
+import UpdateContent from "@/components/dashboard/role-management/Form/UpdateForm/UpdateContent";
+import UpdateForm from "@/components/dashboard/role-management/Form/UpdateForm";
+import userEvent from "@testing-library/user-event";
+
+describe("Create Form Component from Role Management Component",()=>{
+ describe("Rendering",()=>{
+ it('should see Tooltip text when mouse over', async () => {
+ render(
+
+
+
+ )
+ const buttonElement = screen.getByTestId("dialog_tooltip");
+ fireEvent.mouseOver(buttonElement);
+ await waitFor(()=>{
+ expect(screen.queryByText("ویرایش")).toBeVisible();
+ })
+ });
+ })
+})
\ No newline at end of file
diff --git a/src/components/dashboard/role-management/Form/UpdateForm/index.jsx b/src/components/dashboard/role-management/Form/UpdateForm/index.jsx
new file mode 100644
index 0000000..f9defe7
--- /dev/null
+++ b/src/components/dashboard/role-management/Form/UpdateForm/index.jsx
@@ -0,0 +1,31 @@
+import {Dialog, IconButton, Stack, Tooltip} from "@mui/material";
+import {useTranslations} from "next-intl";
+import {useState} from "react";
+import UpdateContent from "@/components/dashboard/role-management/Form/UpdateForm/UpdateContent";
+import EditIcon from "@mui/icons-material/Edit";
+
+const UpdateForm = ({mutate, row}) => {
+ const t = useTranslations();
+ const [openConfirmDialog, setOpenConfirmDialog] = useState(false);
+
+ return (
+
+
+ {
+ setOpenConfirmDialog(true);
+ }}
+ >
+
+
+
+
+
+ )
+}
+export default UpdateForm
\ No newline at end of file
diff --git a/src/components/dashboard/role-management/RoleManagementComponent.jsx b/src/components/dashboard/role-management/RoleManagementComponent.jsx
new file mode 100644
index 0000000..72cba70
--- /dev/null
+++ b/src/components/dashboard/role-management/RoleManagementComponent.jsx
@@ -0,0 +1,94 @@
+import DataTable from "@/core/components/DataTable";
+import {GET_ROLES} from "@/core/data/apiRoutes";
+import TableRowActions from "@/components/dashboard/role-management/TableRowActions";
+import {Box, Typography} from "@mui/material";
+import {useTranslations} from "next-intl";
+import {useMemo} from "react";
+import moment from "jalali-moment";
+import MuiDatePicker from "@/core/components/MuiDatePicker";
+import TableToolbar from "@/components/dashboard/role-management/TableToolbar";
+
+const RoleManagementComponent = () => {
+ const t = useTranslations();
+
+ const columns = useMemo(() => [{
+ accessorFn: (row) => row.id,
+ id: "id",
+ sortDescFirst: true,
+ header: t("RoleManagement.id"),
+ enableColumnFilter: true,
+ datatype: "numeric",
+ filterFn: "equals",
+ columnFilterModeOptions: ["equals", "notEquals", "contains", "lessThan", "greaterThan", "between",],
+ Cell: ({renderedCellValue}) => ({renderedCellValue}),
+ },
+ {
+ accessorFn: (row) => row.name_fa,
+ id: "name_fa",
+ header: t("RoleManagement.name_fa"),
+ enableColumnFilter: true,
+ datatype: "text",
+ filterFn: "contains",
+ columnFilterModeOptions: ["contains", "equals", "notEquals"],
+ Cell: ({renderedCellValue}) => ({renderedCellValue}),
+ },
+ {
+ accessorFn: (row) => row.name,
+ id: "name",
+ header: t("RoleManagement.name"),
+ enableColumnFilter: true,
+ datatype: "text",
+ filterFn: "contains",
+ columnFilterModeOptions: ["contains"],
+ Cell: ({renderedCellValue}) => ({renderedCellValue}),
+ },
+ {
+ accessorFn: (row) => moment(row.created_at).locale("fa").format("HH:mm | yyyy/MM/DD"),
+ id: "created_at",
+ header: t("RoleManagement.created_at"),
+ enableColumnFilter: true,
+ datatype: "date",
+ filterFn: "lessThan",
+ columnFilterModeOptions: ["lessThan", "greaterThan"],
+ Cell: ({renderedCellValue}) => {
+ return {renderedCellValue};
+ },
+ Header: ({column}) => <>{column.columnDef.header}>,
+ Filter: ({column}) => {
+ return ();
+ },
+ }, {
+ accessorFn: (row) => moment(row.updated_at).locale("fa").format("HH:mm | yyyy/MM/DD"),
+ id: "updated_at",
+ header: t("RoleManagement.updated_at"),
+ enableColumnFilter: false,
+ datatype: "numeric",
+ Cell: ({renderedCellValue}) => ({renderedCellValue}),
+ }], []);
+ return (
+
+
+
+ )
+}
+export default RoleManagementComponent
\ No newline at end of file
diff --git a/src/components/dashboard/role-management/TableRowActions.jsx b/src/components/dashboard/role-management/TableRowActions.jsx
new file mode 100644
index 0000000..d655f09
--- /dev/null
+++ b/src/components/dashboard/role-management/TableRowActions.jsx
@@ -0,0 +1,22 @@
+import {Box} from "@mui/material";
+import UpdateForm from "@/components/dashboard/role-management/Form/UpdateForm";
+import DeleteForm from "@/components/dashboard/role-management/Form/DeleteForm";
+
+
+const TableRowActions = ({row, mutate}) => {
+
+ return (
+
+
+
+
+ );
+};
+
+export default TableRowActions;
diff --git a/src/components/dashboard/role-management/TableToolbar.jsx b/src/components/dashboard/role-management/TableToolbar.jsx
new file mode 100644
index 0000000..6a739a7
--- /dev/null
+++ b/src/components/dashboard/role-management/TableToolbar.jsx
@@ -0,0 +1,11 @@
+import {useTranslations} from "next-intl";
+import CreateForm from "@/components/dashboard/role-management/Form/CreateForm";
+
+function TableToolbar({mutate}) {
+ const t = useTranslations();
+
+ return
+
+}
+
+export default TableToolbar;
diff --git a/src/components/dashboard/role-management/__test__/RoleManagementComponent.test.js b/src/components/dashboard/role-management/__test__/RoleManagementComponent.test.js
new file mode 100644
index 0000000..a60dc01
--- /dev/null
+++ b/src/components/dashboard/role-management/__test__/RoleManagementComponent.test.js
@@ -0,0 +1,63 @@
+import {render, screen, waitFor} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../mocks/AppWithProvider";
+import RoleManagementComponent from "@/components/dashboard/role-management/RoleManagementComponent";
+
+describe("Role Management", ()=>{
+ describe("Rendering", ()=>{
+ it('should see id title of data table', async () => {
+ render(
+
+
+
+ )
+ const idEelement = screen.queryByText("کد یکتا")
+ await waitFor(()=>{
+ expect(idEelement).toBeInTheDocument()
+ })
+ });
+ it('should see name title of data table', async () => {
+ render(
+
+
+
+ )
+ const nameEelement = screen.queryByText("نام انگلیسی")
+ await waitFor(()=>{
+ expect(nameEelement).toBeInTheDocument()
+ })
+ });
+ it('should see name_fa title of data table', async () => {
+ render(
+
+
+
+ )
+ const name_faEelement = screen.queryByText("نام فارسی")
+ await waitFor(()=>{
+ expect(name_faEelement).toBeInTheDocument()
+ })
+ });
+ it('should see created at title of data table', async () => {
+ render(
+
+
+
+ )
+ const createdEelement = screen.queryByText("تاریخ درخواست")
+ await waitFor(()=>{
+ expect(createdEelement).toBeInTheDocument()
+ })
+ });
+ it('should see updated at title of data table', async () => {
+ render(
+
+
+
+ )
+ const updateEelement = screen.queryByText("تاریخ بروزرسانی")
+ await waitFor(()=>{
+ expect(updateEelement).toBeInTheDocument()
+ })
+ });
+ })
+})
\ No newline at end of file
diff --git a/src/components/dashboard/role-management/index.jsx b/src/components/dashboard/role-management/index.jsx
new file mode 100644
index 0000000..6376b30
--- /dev/null
+++ b/src/components/dashboard/role-management/index.jsx
@@ -0,0 +1,13 @@
+import RoleManagementComponent from "@/components/dashboard/role-management/RoleManagementComponent";
+import DashboardLayout from "@/layouts/DashboardLayout";
+import moment from "jalali-moment";
+
+function DashboardRoleManagementComponent() {
+ return (
+
+
+
+ );
+}
+
+export default DashboardRoleManagementComponent;
diff --git a/src/components/errors/403.jsx b/src/components/errors/403.jsx
new file mode 100644
index 0000000..ce91443
--- /dev/null
+++ b/src/components/errors/403.jsx
@@ -0,0 +1,37 @@
+import CenterLayout from "@/layouts/CenterLayout";
+import FullPageLayout from "@/layouts/FullPageLayout";
+import {Button, Typography} from "@mui/material";
+import {NextLinkComposed} from "@/core/components/LinkRouting";
+import {useTranslations} from "next-intl";
+import TitlePage from "@/core/components/TitlePage";
+import Svg403 from "@/core/components/svgs/Svg403";
+
+const UnAuthorizedComponent = () => {
+ const t = useTranslations();
+
+ return (
+ <>
+
+
+
+
+
+ {t("ErrorPage.custom_403")}
+
+
+
+
+ >
+ );
+};
+
+export default UnAuthorizedComponent;
diff --git a/src/components/errors/404.jsx b/src/components/errors/404.jsx
new file mode 100644
index 0000000..acfd418
--- /dev/null
+++ b/src/components/errors/404.jsx
@@ -0,0 +1,37 @@
+import CenterLayout from "@/layouts/CenterLayout";
+import FullPageLayout from "@/layouts/FullPageLayout";
+import {Button, Typography} from "@mui/material";
+import {NextLinkComposed} from "@/core/components/LinkRouting";
+import {useTranslations} from "next-intl";
+import TitlePage from "@/core/components/TitlePage";
+import Svg404 from "@/core/components/svgs/Svg404";
+
+const NotFoundComponent = () => {
+ const t = useTranslations();
+
+ return (
+ <>
+
+
+
+
+
+ {t("ErrorPage.custom_404")}
+
+
+
+
+ >
+ );
+};
+
+export default NotFoundComponent;
diff --git a/src/components/errors/500.jsx b/src/components/errors/500.jsx
new file mode 100644
index 0000000..10e9d12
--- /dev/null
+++ b/src/components/errors/500.jsx
@@ -0,0 +1,48 @@
+import CenterLayout from "@/layouts/CenterLayout";
+import FullPageLayout from "@/layouts/FullPageLayout";
+import {Box, Button, Container, Stack, Typography} from "@mui/material";
+import {NextLinkComposed} from "@/core/components/LinkRouting";
+import {useTranslations} from "next-intl";
+import Image from "next/image";
+import TitlePage from "@/core/components/TitlePage";
+
+const ServerErrorComponent = () => {
+ const t = useTranslations();
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {t("ErrorPage.custom_500")}
+
+
+
+
+
+
+ >
+ );
+};
+
+export default ServerErrorComponent;
diff --git a/src/components/first/__tests__/index.test.js b/src/components/first/__tests__/index.test.js
new file mode 100644
index 0000000..8ad9790
--- /dev/null
+++ b/src/components/first/__tests__/index.test.js
@@ -0,0 +1,61 @@
+import {render, screen, waitFor} from "@testing-library/react";
+import FirstComponent from "@/components/first";
+import MockAppWithProviders from "../../../../mocks/AppWithProvider";
+import {server} from "../../../../mocks/server";
+import {rest} from "msw";
+import {GET_USER} from "@/core/data/apiRoutes";
+
+describe("First Component From First Page", () => {
+ describe("Rendering", () => {
+ it("App Name Text Rendered", () => {
+ render();
+ const appNameElement = screen.queryByText(/سامانه CRM/i);
+ expect(appNameElement).toBeInTheDocument()
+ });
+ it("App version Text Rendered", () => {
+ render();
+ const versionControler = screen.queryByText(process.env.NEXT_PUBLIC_API_VERSION, {exact: false});
+ expect(versionControler).toBeInTheDocument()
+ });
+ it("Powered By Rendered With Currect URL", () => {
+ render();
+ const linkElement = screen.queryByText('توسعه یافته توسط وایتل');
+ expect(linkElement).toBeInTheDocument()
+ expect(linkElement).toHaveAttribute('href', process.env.NEXT_PUBLIC_POWERED_BY_URL);
+ });
+ });
+ describe("Behavioral", () => {
+ it("Show Login Button And Do Not Show Dashboard Button When User Is Not Authenticated", async () => {
+ render();
+ await waitFor(() => {
+ const authenticationButtonLogin = screen.queryByText(/ورود کارشناس/i)
+ const authenticationButtonDashboard = screen.queryByText(/داشبورد/i)
+ expect(authenticationButtonLogin).toBeInTheDocument()
+ expect(authenticationButtonDashboard).not.toBeInTheDocument()
+ })
+ });
+ it("Show Dashboard Button And Do Not Show Login Button When User Is Authenticated", async () => {
+ localStorage.setItem("_token", 'token');
+ render();
+ await waitFor(() => {
+ const authenticationButtonLogin = screen.queryByText(/ورود کارشناس/i)
+ const authenticationButtonDashboard = screen.queryByText(/داشبورد/i)
+ expect(authenticationButtonLogin).not.toBeInTheDocument()
+ expect(authenticationButtonDashboard).toBeInTheDocument()
+ })
+ });
+ it("Show Login Button And Do Not Show Dashboard Button When User Authentication Is Expired", async () => {
+ localStorage.setItem("_token", 'token');
+ server.use([rest.get(GET_USER, (req, res, ctx) => {
+ return res(ctx.status(403))
+ })])
+ render();
+ await waitFor(() => {
+ const authenticationButtonLogin = screen.queryByText(/ورود کارشناس/i)
+ const authenticationButtonDashboard = screen.queryByText(/داشبورد/i)
+ expect(authenticationButtonLogin).toBeInTheDocument()
+ expect(authenticationButtonDashboard).not.toBeInTheDocument()
+ })
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/components/first/index.jsx b/src/components/first/index.jsx
new file mode 100644
index 0000000..1d7166d
--- /dev/null
+++ b/src/components/first/index.jsx
@@ -0,0 +1,55 @@
+import LinkRouting, {NextLinkComposed} from "@/core/components/LinkRouting";
+import CenterLayout from "@/layouts/CenterLayout";
+import FullPageLayout from "@/layouts/FullPageLayout";
+import useUser from "@/lib/app/hooks/useUser";
+import {Button, Stack, Typography} from "@mui/material";
+import {useTranslations} from "next-intl";
+import SvgDashboard from "@/core/components/svgs/SvgDashboard";
+
+const FirstComponent = () => {
+ const t = useTranslations();
+ const {isAuth} = useUser();
+
+ return (
+
+
+
+
+ {t("app_name")}
+
+
+
+
+
+ v{process.env.NEXT_PUBLIC_API_VERSION}
+
+
+
+
+ {t("powered_by_witel")}
+
+
+
+ );
+};
+
+export default FirstComponent;
diff --git a/src/components/layouts/Dashboard/Breadcrumbs/BreadcrumbItem.jsx b/src/components/layouts/Dashboard/Breadcrumbs/BreadcrumbItem.jsx
new file mode 100644
index 0000000..c233be3
--- /dev/null
+++ b/src/components/layouts/Dashboard/Breadcrumbs/BreadcrumbItem.jsx
@@ -0,0 +1,19 @@
+import LinkRouting from "@/core/components/LinkRouting";
+import {Typography} from "@mui/material";
+import {useTranslations} from "next-intl";
+
+export default function BreadcrumbItem(props) {
+ const t = useTranslations();
+ const isLast = props.index === props.RouterArray.length - 1;
+ const url = `/${props.RouterArray.slice(0, props.index + 1).join("/")}`;
+
+ return isLast ? (
+
+ {t("sidebar." + props.label)}
+
+ ) : (
+
+ {t("sidebar." + props.label)}
+
+ );
+}
diff --git a/src/components/layouts/Dashboard/Breadcrumbs/index.jsx b/src/components/layouts/Dashboard/Breadcrumbs/index.jsx
new file mode 100644
index 0000000..9ab234e
--- /dev/null
+++ b/src/components/layouts/Dashboard/Breadcrumbs/index.jsx
@@ -0,0 +1,49 @@
+import {useRouter} from "next/router";
+import {Box, Breadcrumbs} from "@mui/material";
+import {useTheme} from "@mui/material/styles";
+import {NavigateBefore, NavigateNext} from "@mui/icons-material";
+import BreadcrumbItem from "./BreadcrumbItem";
+
+const BreadCrumbs = (props) => {
+ const {isVisible} = props;
+ const theme = useTheme();
+ const router = useRouter();
+
+ if (!isVisible) {
+ return null;
+ }
+
+ const {pathname} = router;
+ const RouterArray = pathname.split("/").filter((segment) => segment !== "");
+
+ if (RouterArray.length === 1) {
+ return null;
+ }
+
+ return (
+
+
+ ) : (
+
+ )
+ }
+ aria-label="breadcrumb"
+ >
+ {RouterArray.map((segment, index) => (
+
+ ))}
+
+
+ );
+};
+
+export default BreadCrumbs;
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/ActionHeader/__test__/index.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/ActionHeader/__test__/index.test.js
new file mode 100644
index 0000000..69f9b52
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/ActionHeader/__test__/index.test.js
@@ -0,0 +1,43 @@
+import {render, screen, waitFor} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider";
+import {AnswersProvider} from "@/lib/callWidget/contexts/answers";
+import ActionHeader from "..";
+
+const tab = {
+ active: true,
+ active_category_id: 1,
+ phone_number : "09134849737"
+}
+
+describe('Action Header Button from Call Actions Categories', () => {
+ describe('Rendering', () => {
+ it('should see header text', async () => {
+ render(
+
+
+
+
+
+ )
+ const headingElement = screen.getByRole('heading', {
+ name: /\.\.\.\. عملیات های مربوط به تماس:/i
+ })
+ await waitFor(() => {
+ expect(headingElement).toBeInTheDocument()
+ })
+ });
+ it('should see phone number', async () => {
+ render(
+
+
+
+
+
+ )
+ const phone_numberElement = screen.queryByText(/09134849737/i)
+ await waitFor(() => {
+ expect(phone_numberElement).toBeInTheDocument()
+ })
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/ActionHeader/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/ActionHeader/index.jsx
new file mode 100644
index 0000000..97b4297
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/ActionHeader/index.jsx
@@ -0,0 +1,25 @@
+import {useTranslations} from "next-intl";
+import {Stack, Typography} from "@mui/material";
+
+const ActionHeader = ({tab}) => {
+ const t = useTranslations();
+ return (
+
+
+ .... {t("CallAction.call_history_of")}:
+
+
+ {tab.phone_number} ....
+
+
+ )
+}
+
+export default ActionHeader
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/CallActionCategoriesButton.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/CallActionCategoriesButton.jsx
new file mode 100644
index 0000000..40c2b18
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/CallActionCategoriesButton.jsx
@@ -0,0 +1,25 @@
+import { Button, Grid } from "@mui/material";
+import useCategories from "@/lib/callWidget/hooks/useCategories";
+
+const CallActionCategoriesButton = ({ category, tab }) => {
+ const { setActiveCategoryID } = useCategories();
+
+ return (
+
+
+
+
+
+ );
+};
+export default CallActionCategoriesButton;
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/__test__/CallActionCategoriesButton.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/__test__/CallActionCategoriesButton.test.js
new file mode 100644
index 0000000..195df02
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/__test__/CallActionCategoriesButton.test.js
@@ -0,0 +1,51 @@
+import {fireEvent, render, screen, waitFor} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider";
+import {AnswersProvider} from "@/lib/callWidget/contexts/answers";
+import CallActionCategoriesButton from "../CallActionCategoriesButton";
+
+const category = {
+ category_id: 1,
+ category_name: "اطلاعات راه ها",
+}
+const tab = {
+ active: true,
+ active_category_id: 1,
+}
+
+describe('Call Action Categories Button from Call Actions Categories', () => {
+ describe('Rendering', () => {
+ it('should categories names', async () => {
+ render(
+
+
+
+
+
+ )
+ const categoryElement = screen.queryByText("اطلاعات راه ها")
+ await waitFor(() => {
+ expect(categoryElement).toBeInTheDocument()
+ })
+ });
+ });
+ describe("Behavior", ()=>{
+ it("Should change the color when button clecked",async()=>{
+ render(
+
+
+
+
+
+ )
+ const buttonElement = screen.getByRole('button', {
+ name: /اطلاعات راه ها/i
+ })
+
+ fireEvent.click(buttonElement);
+
+ await waitFor(() => {
+ expect(buttonElement).toHaveStyle('background-color: rgb(12, 31, 23)');
+ })
+ })
+ })
+})
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/__test__/index.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/__test__/index.test.js
new file mode 100644
index 0000000..ee223ff
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/__test__/index.test.js
@@ -0,0 +1,29 @@
+import { AnswersProvider } from "@/lib/callWidget/contexts/answers";
+import CallActionsCategories from "..";
+import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider";
+import { render, screen, waitFor } from "@testing-library/react";
+
+const tab = {
+ active: true,
+ active_category_id: 1,
+ id : 1,
+ phone_number : "09134849737"
+}
+
+describe('Call Action Categories Button from Call Actions Categories', () => {
+ describe('Rendering', () => {
+ it('should see divider text', async () => {
+ render(
+
+
+
+
+
+ )
+ const divider_textElement = screen.queryByText("موضوع ها")
+ await waitFor(() => {
+ expect(divider_textElement).toBeInTheDocument()
+ })
+ });
+ });
+})
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/index.jsx
new file mode 100644
index 0000000..dc814ec
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionCategories/index.jsx
@@ -0,0 +1,22 @@
+import { Chip, Divider, Grid, Stack } from "@mui/material";
+import useCategories from "@/lib/callWidget/hooks/useCategories";
+import CallActionCategoriesButton from "./CallActionCategoriesButton";
+import { useTranslations } from "next-intl";
+
+const CallActionsCategories = ({ tab }) => {
+ const { categoryLists } = useCategories();
+ const t = useTranslations();
+ return (
+ <>
+
+
+
+ {categoryLists.map((category) => {
+ return ;
+ })}
+
+
+ >
+ );
+};
+export default CallActionsCategories;
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionDescription/__test__/index.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionDescription/__test__/index.test.js
new file mode 100644
index 0000000..b7e0543
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionDescription/__test__/index.test.js
@@ -0,0 +1,54 @@
+import {fireEvent, render, screen, waitFor} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider";
+import {AnswersProvider} from "@/lib/callWidget/contexts/answers";
+import CallActionDescription from "..";
+
+describe('Action Header Button from Call Actions Categories', () => {
+ describe('Rendering', () => {
+ it('should see placeholder text', async () => {
+ render(
+
+
+
+
+
+ )
+ const textElement = screen.getByPlaceholderText('لطفا توضیحات خود را وارد نمایید')
+ await waitFor(() => {
+ expect(textElement).toBeInTheDocument()
+ })
+ });
+ it('should see header text', async () => {
+ render(
+
+
+
+
+
+ )
+ const textElement = screen.getByText('توضیحات تماس (اختیاری)')
+ await waitFor(() => {
+ expect(textElement).toBeInTheDocument()
+ })
+ });
+ });
+ describe("Behavior", ()=>{
+ it("Should see what write in input", async()=>{
+ const setDescription = jest.fn();
+ render(
+
+
+
+
+
+ )
+ const textElement = screen.getByPlaceholderText('لطفا توضیحات خود را وارد نمایید')
+
+ fireEvent.change(textElement, {target : {value : "امین"}})
+
+ await waitFor(() => {
+ expect(textElement).toHaveValue("امین")
+ })
+ })
+ })
+});
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionDescription/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionDescription/index.jsx
new file mode 100644
index 0000000..6a29324
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionDescription/index.jsx
@@ -0,0 +1,27 @@
+import { Box, Chip, Divider, TextField } from "@mui/material"
+import { useTranslations } from "next-intl";
+
+const CallActionDescription = ({setDescription}) => {
+ const t = useTranslations();
+ return(
+ <>
+
+
+ {
+ setDescription(e.target.value)
+ }}
+ />
+
+ >
+ )
+}
+export default CallActionDescription
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/CallActionSubcategoriesButton.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/CallActionSubcategoriesButton.jsx
new file mode 100644
index 0000000..2b4a479
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/CallActionSubcategoriesButton.jsx
@@ -0,0 +1,14 @@
+import { Button, Grid } from "@mui/material";
+
+const CallActionSubcategoriesButton = ({ sub_category, tab, handleSubcategoryButton }) => {
+
+ return sub_category.category_id === tab.active_category_id ? (
+
+
+
+ ) : null;
+};
+export default CallActionSubcategoriesButton;
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/__test__/CallActionSubcategoriesButton.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/__test__/CallActionSubcategoriesButton.test.js
new file mode 100644
index 0000000..718f7cd
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/__test__/CallActionSubcategoriesButton.test.js
@@ -0,0 +1,37 @@
+import {render, screen, waitFor} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider";
+import {AnswersProvider} from "@/lib/callWidget/contexts/answers";
+import CallActionSubcategoriesButton from "../CallActionSubcategoriesButton";
+
+const sub_category = {
+ category_id: 1,
+ category_name: "اطلاعات راه",
+ subcategory_id: 1,
+ subcategory_name: "اطلاعات باز و بسته"
+};
+
+const tab = {
+ active: true,
+ active_category_id: 1,
+ date: new Date(),
+ id: 1,
+ phone_number: "09134849737"
+};
+
+describe('Call Action Subcategories Button from Call Actions Categories', () => {
+ describe('Rendering', () => {
+ it('should render subcategory name', async () => {
+ render(
+
+
+
+
+
+ );
+ const subCategoryElement = screen.queryByText(/اطلاعات باز و بسته/i);
+ await waitFor(() => {
+ expect(subCategoryElement).toBeInTheDocument();
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/__test__/index.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/__test__/index.test.js
new file mode 100644
index 0000000..2195bde
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/__test__/index.test.js
@@ -0,0 +1,29 @@
+import { AnswersProvider } from "@/lib/callWidget/contexts/answers";
+import CallActionsSubcategories from "..";
+import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider";
+import { render, screen, waitFor } from "@testing-library/react";
+
+const tab = {
+ active: true,
+ active_category_id: 1,
+ id : 1,
+ phone_number : "09134849737"
+}
+
+describe('Call Action Categories Button from Call Actions Categories', () => {
+ describe('Rendering', () => {
+ it('should see divider text', async () => {
+ render(
+
+
+
+
+
+ )
+ const divider_textElement = screen.queryByText("زیر موضوع ها")
+ await waitFor(() => {
+ expect(divider_textElement).toBeInTheDocument()
+ })
+ });
+ });
+})
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/index.jsx
new file mode 100644
index 0000000..0b88cfa
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/index.jsx
@@ -0,0 +1,22 @@
+import useCategories from "@/lib/callWidget/hooks/useCategories";
+import { Chip, Divider, Grid, Stack } from "@mui/material";
+import CallActionSubcategoriesButton from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/CallActionSubcategories/CallActionSubcategoriesButton";
+import { useTranslations } from "next-intl";
+
+const CallActionsSubcategories = ({ tab, handleSubcategoryButton }) => {
+ const { subCategoryLists } = useCategories();
+ const t = useTranslations();
+ return (
+ <>
+
+
+
+ {subCategoryLists.map((sub_category, index) => {
+ return ;
+ })}
+
+
+ >
+ );
+};
+export default CallActionsSubcategories;
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/index.jsx
new file mode 100644
index 0000000..f5c76b0
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions/index.jsx
@@ -0,0 +1,43 @@
+import CallActionsCategories from "./CallActionCategories";
+import CallActionsSubcategories from "./CallActionSubcategories";
+import ActionHeader from "./ActionHeader";
+import CallActionDescription from "./CallActionDescription";
+import { CALL_ACTION } from "@/core/data/apiRoutes";
+import useRequest from "@/lib/app/hooks/useRequest";
+import useAnswers from "@/lib/callWidget/hooks/useAnswers";
+import { useState } from "react";
+
+function CallActions({ tab }) {
+ const [description, setDescription] = useState('')
+ const requestServer = useRequest({ auth: true });
+ const { closeAnswerTab } = useAnswers();
+
+ const handleSubcategoryButton = (category_id, subcategory_id) => {
+ requestServer(`${CALL_ACTION}/${tab.id}`, "post", {
+ data: {
+ category_id,
+ subcategory_id,
+ description
+ },
+ success: {
+ notification: { show: false },
+ },
+ })
+ .then(() => {
+ closeAnswerTab(tab.id);
+ })
+ .catch(() => {});
+ };
+
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default CallActions;
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ErrorOrEmpty/__tests__/index.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ErrorOrEmpty/__tests__/index.test.js
new file mode 100644
index 0000000..91d5651
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ErrorOrEmpty/__tests__/index.test.js
@@ -0,0 +1,19 @@
+import {render, screen} from "@testing-library/react";
+import ErrorOrEmpty from "../../ErrorOrEmpty";
+import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider";
+
+describe("ErrorOrEmpty Component From call history", () => {
+ describe("Rendering", () => {
+ it("error fetching history of call text rendered", () => {
+ render();
+ const errorText = screen.queryByText('خطا در دریافت تاریخچه تماس دریافتی');
+ expect(errorText).toBeInTheDocument();
+ });
+ it("empty fetching history of call text rendered", () => {
+ render();
+ const emptyText = screen.queryByText('تاریخچه ای برای تماس دریافتی یافت نشد');
+ expect(emptyText).toBeInTheDocument();
+
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ErrorOrEmpty/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ErrorOrEmpty/index.jsx
new file mode 100644
index 0000000..bb0f818
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ErrorOrEmpty/index.jsx
@@ -0,0 +1,33 @@
+import {useTranslations} from "next-intl";
+import {Typography} from "@mui/material";
+
+const ErrorOrEmpty = ({isErrorOrEmpty}) => {
+ const t = useTranslations();
+ return (
+ <>
+ {isErrorOrEmpty === "error" ?
+
+ {t("CallHistory.call_history_error_fetching")}
+
+ : isErrorOrEmpty === "empty" ?
+
+ {t("CallHistory.call_history_not_found")}
+ : <>>
+ }
+ >
+ )
+}
+
+export default ErrorOrEmpty
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/HistoryHeader/__tests__/index.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/HistoryHeader/__tests__/index.test.js
new file mode 100644
index 0000000..51d700e
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/HistoryHeader/__tests__/index.test.js
@@ -0,0 +1,21 @@
+import {render, screen} from "@testing-library/react";
+import HistoryHeader from "../../HistoryHeader";
+import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider";
+
+describe("HistoryHeader Component From call history", () => {
+ describe("Rendering", () => {
+ const tab = {
+ phone_number: "09111111111"
+ }
+ it("text of header rendered", () => {
+ render();
+ const textHeader = screen.queryByText(/تاریخچه تماس های/i);
+ expect(textHeader).toBeInTheDocument();
+ });
+ it("caller number in header rendered", () => {
+ render();
+ const callerNumber = screen.queryByText(/09111111111/i);
+ expect(callerNumber).toBeInTheDocument();
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/HistoryHeader/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/HistoryHeader/index.jsx
new file mode 100644
index 0000000..21a3270
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/HistoryHeader/index.jsx
@@ -0,0 +1,25 @@
+import {useTranslations} from "next-intl";
+import {Stack, Typography} from "@mui/material";
+
+const HistoryHeader = ({tab}) => {
+ const t = useTranslations();
+ return (
+
+
+ .... {t("CallHistory.call_history_of")}:
+
+
+ {tab.phone_number} ....
+
+
+ )
+}
+
+export default HistoryHeader
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ListItemOfCalls/PreviousOperatorData/__tests__/index.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ListItemOfCalls/PreviousOperatorData/__tests__/index.test.js
new file mode 100644
index 0000000..4fd4519
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ListItemOfCalls/PreviousOperatorData/__tests__/index.test.js
@@ -0,0 +1,22 @@
+import {render, screen} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../../../../../../../mocks/AppWithProvider";
+import PreviousOperatorData from "../../PreviousOperatorData";
+
+describe("PreviousOperatorData Component From call history", () => {
+ describe("Rendering", () => {
+ const historyItem = {
+ operator_name: "test name",
+ answer_date: "test date"
+ }
+ it("operator name rendered", () => {
+ render();
+ const operatorName = screen.queryByText("test name");
+ expect(operatorName).toBeInTheDocument();
+ });
+ it("operator answer date rendered", () => {
+ render();
+ const operatorAnswerDate = screen.queryByText("test date");
+ expect(operatorAnswerDate).toBeInTheDocument();
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ListItemOfCalls/PreviousOperatorData/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ListItemOfCalls/PreviousOperatorData/index.jsx
new file mode 100644
index 0000000..afd2ad7
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ListItemOfCalls/PreviousOperatorData/index.jsx
@@ -0,0 +1,28 @@
+import {useTranslations} from "next-intl";
+import {Avatar, Box, ListItem, ListItemAvatar, ListItemText, Typography} from "@mui/material";
+import HeadsetMicIcon from '@mui/icons-material/HeadsetMic';
+
+const PreviousOperatorData = ({historyItem}) => {
+ const t = useTranslations();
+ return (
+
+
+
+
+
+
+
+
+ {historyItem.operator_name}
+
+
+ ({t("CallHistory.operator")})
+
+
+ } secondary={historyItem.answer_date}/>
+
+ )
+}
+
+export default PreviousOperatorData
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ListItemOfCalls/Topics/__tests__/index.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ListItemOfCalls/Topics/__tests__/index.test.js
new file mode 100644
index 0000000..d9d77e4
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ListItemOfCalls/Topics/__tests__/index.test.js
@@ -0,0 +1,22 @@
+import {render, screen} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../../../../../../../mocks/AppWithProvider";
+import Topics from "../../Topics";
+
+describe("Topics Component From call history", () => {
+ describe("Rendering", () => {
+ const historyItem = {
+ category_name: "test category",
+ subcategory_name: "test sub category"
+ }
+ it("operator name rendered", () => {
+ render();
+ const operatorName = screen.queryByText("test category");
+ expect(operatorName).toBeInTheDocument();
+ });
+ it("operator answer date rendered", () => {
+ render();
+ const operatorAnswerDate = screen.queryByText("test sub category");
+ expect(operatorAnswerDate).toBeInTheDocument();
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ListItemOfCalls/Topics/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ListItemOfCalls/Topics/index.jsx
new file mode 100644
index 0000000..3b57744
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ListItemOfCalls/Topics/index.jsx
@@ -0,0 +1,43 @@
+import {useTranslations} from "next-intl";
+import {Box, List, ListItem, Typography} from "@mui/material";
+
+const Topics = ({historyItem}) => {
+ const t = useTranslations();
+ return (
+
+
+
+
+
+ {t("CallHistory.category_name")}:
+
+
+ {historyItem.category_name}
+
+
+
+
+
+
+
+
+ {t("CallHistory.subcategory_name")}:
+
+
+ {historyItem.subcategory_name}
+
+
+
+
+
+ )
+}
+
+export default Topics
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ListItemOfCalls/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ListItemOfCalls/index.jsx
new file mode 100644
index 0000000..7af4fd5
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/ListItemOfCalls/index.jsx
@@ -0,0 +1,21 @@
+import {useTranslations} from "next-intl";
+import {Grow, List} from "@mui/material";
+import PreviousOperatorData from "./PreviousOperatorData";
+import Topics from "./Topics";
+
+const ListItemOfCalls = ({historyItem}) => {
+ const t = useTranslations();
+
+ // console.log(historyItem)
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+export default ListItemOfCalls
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/LoadingHistory/__tests__/intex.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/LoadingHistory/__tests__/intex.test.js
new file mode 100644
index 0000000..1592a02
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/LoadingHistory/__tests__/intex.test.js
@@ -0,0 +1,68 @@
+import {render, screen} from "@testing-library/react";
+import LoadingHistory from "../../LoadingHistory";
+import MockAppWithProviders from "../../../../../../../../../../../mocks/AppWithProvider";
+
+describe("LoadingHistory Component From call history", () => {
+ describe("Rendering", () => {
+ it("loading list rendered", () => {
+ render();
+ const loadingList = screen.queryByTestId("loading-list");
+ expect(loadingList).toBeInTheDocument();
+ });
+ it("loading list item avatar rendered", () => {
+ render();
+ const loadingListItemAvatar = screen.queryByTestId("loading-listItemAvatar");
+ expect(loadingListItemAvatar).toBeInTheDocument();
+ });
+ it("loading list item text rendered", () => {
+ render();
+ const loadingListItemText = screen.queryByTestId("loading-listItemText");
+ expect(loadingListItemText).toBeInTheDocument();
+ });
+ it("loading list item category rendered", () => {
+ render();
+ const loadingListItemCat = screen.queryByTestId("loading-listItem-cat");
+ expect(loadingListItemCat).toBeInTheDocument();
+ });
+ it("loading list item sub category rendered", () => {
+ render();
+ const loadingListItemSubCat = screen.queryByTestId("loading-listItem-subCat");
+ expect(loadingListItemSubCat).toBeInTheDocument();
+ });
+ it("operator avatar skeleton rendered", () => {
+ render();
+ const operatorAvatarSkeleton = screen.queryByTestId("operator-avatar-skeleton");
+ expect(operatorAvatarSkeleton).toBeInTheDocument();
+ });
+ it("operator name skeleton rendered", () => {
+ render();
+ const operatorNameSkeleton = screen.queryByTestId("operator-name-skeleton");
+ expect(operatorNameSkeleton).toBeInTheDocument();
+ });
+ it("operator answer date skeleton rendered", () => {
+ render();
+ const operatorAnswerDateSkeleton = screen.queryByTestId("operator-answer-date-skeleton");
+ expect(operatorAnswerDateSkeleton).toBeInTheDocument();
+ });
+ it("operator category header skeleton rendered", () => {
+ render();
+ const operatorCategoryHeaderSkeleton = screen.queryByTestId("operator-category-header-skeleton");
+ expect(operatorCategoryHeaderSkeleton).toBeInTheDocument();
+ });
+ it("operator category value skeleton rendered", () => {
+ render();
+ const operatorCategoryValueSkeleton = screen.queryByTestId("operator-category-value-skeleton");
+ expect(operatorCategoryValueSkeleton).toBeInTheDocument();
+ });
+ it("operator subCategory header skeleton rendered", () => {
+ render();
+ const operatorSubCategoryHeaderSkeleton = screen.queryByTestId("operator-subCategory-header-skeleton");
+ expect(operatorSubCategoryHeaderSkeleton).toBeInTheDocument();
+ });
+ it("operator subCategory value skeleton rendered", () => {
+ render();
+ const operatorSubCategoryValueSkeleton = screen.queryByTestId("operator-subCategory-value-skeleton");
+ expect(operatorSubCategoryValueSkeleton).toBeInTheDocument();
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/LoadingHistory/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/LoadingHistory/index.jsx
new file mode 100644
index 0000000..2c9900e
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/LoadingHistory/index.jsx
@@ -0,0 +1,86 @@
+import {useTranslations} from "next-intl";
+import {Box, List, ListItem, ListItemAvatar, ListItemText, Skeleton} from "@mui/material";
+
+const LoadingHistory = () => {
+ const t = useTranslations();
+ return (
+ <>
+
+
+
+
+
+
+
+
+ } secondary={
+
+ }/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+
+ )
+}
+
+export default LoadingHistory
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/index.jsx
new file mode 100644
index 0000000..c83bc49
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory/index.jsx
@@ -0,0 +1,65 @@
+import {useTranslations} from "next-intl";
+import {Box, Chip, Divider, Stack} from "@mui/material";
+import HeadphonesIcon from '@mui/icons-material/Headphones';
+import LoadingHistory from "./LoadingHistory";
+import HistoryHeader from "./HistoryHeader";
+import ErrorOrEmpty from "./ErrorOrEmpty";
+import useCallerHistory from "@/lib/callWidget/hooks/useCallerHistory";
+import ListItemOfCalls from "./ListItemOfCalls";
+import {useState} from "react";
+
+const max_size = 10;
+const CallHistory = ({tab}) => {
+ const t = useTranslations();
+ const {
+ firstItemOfHistory,
+ historyList,
+ isLoadingHistoryList,
+ errorHistoryList
+ } = useCallerHistory(tab.id, tab.phone_number, max_size);
+ const [showRestOfHistory, setShowRestOfHistory] = useState(false);
+
+ return (
+
+
+
+ {errorHistoryList ? (
+
+ ) : isLoadingHistoryList ? (
+
+ ) : !firstItemOfHistory ? (
+
+ ) : (
+
+
+ {historyList && (
+
+ }
+ onClick={() => setShowRestOfHistory(true)}
+ label={t("show_more")}
+ disabled={showRestOfHistory}
+ />
+
+ )}
+ {showRestOfHistory && (
+ <>
+ {historyList.map((historyItem, index) => (
+ <>
+
+
+ >
+ ))}
+ >)
+ }
+
+ )}
+
+
+ )
+}
+
+export default CallHistory
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/index.jsx
new file mode 100644
index 0000000..8fc1d87
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/index.jsx
@@ -0,0 +1,23 @@
+import {Divider, Grid} from "@mui/material";
+import CallActions from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions";
+import CallHistory from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory";
+
+const CallTabPanel = ({tab}) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default CallTabPanel
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/CallTabDetails.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/CallTabDetails.jsx
new file mode 100644
index 0000000..9427201
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/CallTabDetails.jsx
@@ -0,0 +1,17 @@
+import {Stack, Typography} from "@mui/material";
+import CallIcon from '@mui/icons-material/Call';
+import CallTabTime from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/CallTabTime";
+
+const CallTabDetails = ({tab}) => {
+
+ return (
+
+
+
+ {tab.phone_number}
+
+
+
+ )
+}
+export default CallTabDetails
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/CallTabLabel.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/CallTabLabel.jsx
new file mode 100644
index 0000000..e49bcdc
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/CallTabLabel.jsx
@@ -0,0 +1,30 @@
+import {IconButton, Stack, Zoom} from "@mui/material";
+import CloseIcon from '@mui/icons-material/Close';
+import useAnswers from "@/lib/callWidget/hooks/useAnswers";
+import CallTabDetails
+ from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/CallTabDetails";
+
+const CallTabLabel = ({tab, index}) => {
+ const {closeAnswerTab, activeTab} = useAnswers()
+ const handleCloseClick = (id) => {
+ closeAnswerTab(id)
+ };
+
+ return (
+
+
+
+ handleCloseClick(tab.id)}>
+
+
+
+
+ )
+}
+
+export default CallTabLabel
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/CallTabTime.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/CallTabTime.jsx
new file mode 100644
index 0000000..5d627e4
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/CallTabTime.jsx
@@ -0,0 +1,29 @@
+import {Typography} from "@mui/material";
+import {useEffect, useState} from "react";
+import moment from "jalali-moment";
+import useLanguage from "@/lib/app/hooks/useLanguage";
+
+const CallTabTime = ({realTimeDate}) => {
+ const {languageApp} = useLanguage()
+ const [date, setDate] = useState('...')
+ const changeTabTime = (tabTime) => {
+ moment.relativeTimeThreshold('ss', 1)
+ return moment(tabTime).locale(languageApp).fromNow()
+ }
+
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setDate(changeTabTime(realTimeDate))
+ }, 1000)
+
+ return () => {
+ clearInterval(timer)
+ }
+ }, [realTimeDate]);
+
+ return (
+ {date}
+ )
+}
+export default CallTabTime
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/__test__/CallTabDetails.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/__test__/CallTabDetails.test.js
new file mode 100644
index 0000000..7d50bff
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/__test__/CallTabDetails.test.js
@@ -0,0 +1,28 @@
+import {render, screen, waitFor} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../../../../mocks/AppWithProvider";
+import CallTabDetails
+ from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/CallTabDetails";
+
+
+const tab = {
+ phone_number: "09134849737",
+ id: 1,
+ date: new Date()
+}
+
+describe("Call Tab Details from Call Tan List", () => {
+ describe("Rendering", () => {
+ it('Should see the phone number on tab detail', async () => {
+ render(
+
+
+
+ )
+ const phoneNumberElement = screen.getByText("09134849737")
+ await waitFor(() => {
+ expect(phoneNumberElement).toHaveTextContent("09134849737")
+ })
+ });
+
+ })
+})
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/__test__/CallTabTime.test.js b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/__test__/CallTabTime.test.js
new file mode 100644
index 0000000..31b9a1b
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/__test__/CallTabTime.test.js
@@ -0,0 +1,25 @@
+import {render, screen, waitFor} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../../../../mocks/AppWithProvider";
+import CallTabTime from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/CallTabTime";
+
+const tab = {
+ phone_number: "09134849737",
+ id: 1,
+ date: new Date()
+}
+
+describe("Call Tab Details from Call Tan List", () => {
+ describe("Rendering", () => {
+ it('Should see the date on tab detail', async () => {
+ render(
+
+
+
+ )
+
+ await waitFor(() => {
+ expect(screen.getByText(/ثانیه پیش/i)).toBeInTheDocument()
+ })
+ });
+ })
+})
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/index.jsx
new file mode 100644
index 0000000..ff26544
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/index.jsx
@@ -0,0 +1,33 @@
+import {Box, Tab, Tabs} from "@mui/material";
+import CallTabLabel
+ from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/CallTabLabel";
+import useAnswers from "@/lib/callWidget/hooks/useAnswers";
+
+const CallTabsList = () => {
+ const {answersList, changeActiveTabAnswers, activeTab} = useAnswers()
+ const handleChange = (event, newValue) => {
+ changeActiveTabAnswers(newValue)
+ };
+
+ return (
+
+
+ {answersList.map((answer, index) => (
+ } key = {answer.id}/>
+ ))}
+
+
+ )
+}
+
+export default CallTabsList
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/index.jsx
new file mode 100644
index 0000000..8a96506
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/index.jsx
@@ -0,0 +1,17 @@
+import CallTabPanel from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel";
+import useAnswers from "@/lib/callWidget/hooks/useAnswers";
+import CallTabsList from "src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList";
+
+const CallTabs = () => {
+ const {answersList} = useAnswers()
+
+ return (
+ <>
+
+ {answersList.map((answer) => (
+
+ ))}
+ >
+ )
+}
+export default CallTabs
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/index.jsx b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/index.jsx
new file mode 100644
index 0000000..e7d4278
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/index.jsx
@@ -0,0 +1,49 @@
+import {Dialog} from "@mui/material";
+import {DialogTransition} from "@/core/components/DialogTransition";
+import {useEffect} from "react";
+import useCallDialog from "@/lib/callWidget/hooks/useCallDialog";
+import useAnswers from "@/lib/callWidget/hooks/useAnswers";
+import useSocket from "@/lib/app/hooks/useSocket";
+import CallTabs from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs";
+
+const CallWidgetDialog = () => {
+ const {openCallDialog, setOpenCallDialog} = useCallDialog()
+ const {answersList, newAnswerTab} = useAnswers()
+ const socket = useSocket()
+
+ useEffect(() => {
+ const answerCall = (_data, act) => {
+ const data = JSON.parse(_data)
+ newAnswerTab({
+ id: data.call_id,
+ phone_number: data.phone_number
+ })
+ act()
+ }
+ socket.on('answer', answerCall)
+
+ return () => {
+ socket.off('answer', answerCall)
+ }
+ });
+
+ useEffect(() => {
+ if (answersList.length === 0) {
+ setOpenCallDialog(false)
+ return
+ }
+ setOpenCallDialog(true)
+ }, [answersList.length]);
+
+ return (
+
+ )
+}
+
+export default CallWidgetDialog
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/CallWidget/index.jsx b/src/components/layouts/Dashboard/CallWidget/index.jsx
new file mode 100644
index 0000000..1480498
--- /dev/null
+++ b/src/components/layouts/Dashboard/CallWidget/index.jsx
@@ -0,0 +1,26 @@
+import CallWidgetDialog from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog";
+import {AnswersProvider} from "@/lib/callWidget/contexts/answers";
+import useSocket from "@/lib/app/hooks/useSocket";
+import {useEffect} from "react";
+
+const CallWidget = () => {
+ const socket = useSocket()
+
+ useEffect(() => {
+ if (socket.connected) return
+
+ socket.connect()
+
+ return () => {
+ socket.disconnect()
+ }
+ }, []);
+
+ return (
+
+
+
+ )
+}
+
+export default CallWidget
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/Header/ProfileData.jsx b/src/components/layouts/Dashboard/Header/ProfileData.jsx
new file mode 100644
index 0000000..548378b
--- /dev/null
+++ b/src/components/layouts/Dashboard/Header/ProfileData.jsx
@@ -0,0 +1,18 @@
+import {Avatar, Stack, Typography} from "@mui/material";
+import useUser from "@/lib/app/hooks/useUser";
+
+export default function ProfileData() {
+ const {user} = useUser();
+ return (
+
+
+
+ {user.username}
+
+
+ );
+}
diff --git a/src/components/layouts/Dashboard/Header/ProfileMenu.jsx b/src/components/layouts/Dashboard/Header/ProfileMenu.jsx
new file mode 100644
index 0000000..415d501
--- /dev/null
+++ b/src/components/layouts/Dashboard/Header/ProfileMenu.jsx
@@ -0,0 +1,62 @@
+import {Avatar, IconButton, Menu, Tooltip} from "@mui/material";
+import {useState} from "react";
+import ProfileData from "./ProfileData";
+import ProfileOptions from "./ProfileOptions";
+import {useTranslations} from "next-intl";
+import useUser from "@/lib/app/hooks/useUser";
+
+function ProfileMenu() {
+ const t = useTranslations();
+ const [anchorElUser, setAnchorElUser] = useState(null);
+ const {user} = useUser();
+
+ const handleOpenUserMenu = (event) => {
+ setAnchorElUser(event.currentTarget);
+ };
+ const handleCloseUserMenu = () => {
+ setAnchorElUser(null);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+}
+
+export default ProfileMenu;
diff --git a/src/components/layouts/Dashboard/Header/ProfileOptionLogOut.jsx b/src/components/layouts/Dashboard/Header/ProfileOptionLogOut.jsx
new file mode 100644
index 0000000..2a6cf85
--- /dev/null
+++ b/src/components/layouts/Dashboard/Header/ProfileOptionLogOut.jsx
@@ -0,0 +1,49 @@
+import {Box, Button, MenuItem, Typography} from "@mui/material";
+import MeetingRoomIcon from "@mui/icons-material/MeetingRoom";
+import useUser from "@/lib/app/hooks/useUser";
+import {useTranslations} from "next-intl";
+
+export default function ProfileOptionLogOut({handleCloseUserMenu}) {
+ const t = useTranslations();
+ const {clearToken} = useUser();
+ const handleClickLogOut = () => {
+ handleCloseUserMenu();
+ clearToken();
+ };
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/src/components/layouts/Dashboard/Header/ProfileOptions.jsx b/src/components/layouts/Dashboard/Header/ProfileOptions.jsx
new file mode 100644
index 0000000..6e7bdba
--- /dev/null
+++ b/src/components/layouts/Dashboard/Header/ProfileOptions.jsx
@@ -0,0 +1,49 @@
+import {Box, MenuItem, Typography} from "@mui/material";
+import {NextLinkComposed} from "@/core/components/LinkRouting";
+import {useTranslations} from "next-intl";
+import headerProfileItems from "@/core/data/headerProfileItems";
+import ProfileOptionLogOut from "./ProfileOptionLogOut";
+
+export default function ProfileOptions({handleCloseUserMenu}) {
+ const t = useTranslations();
+
+ return (
+ <>
+ {headerProfileItems.map((profile_item) => (
+
+ ))}
+
+ >
+ );
+}
diff --git a/src/components/layouts/Dashboard/Header/index.jsx b/src/components/layouts/Dashboard/Header/index.jsx
new file mode 100644
index 0000000..e8af065
--- /dev/null
+++ b/src/components/layouts/Dashboard/Header/index.jsx
@@ -0,0 +1,74 @@
+import MenuIcon from "@mui/icons-material/Menu";
+import ProfileMenu from "./ProfileMenu";
+import {useTheme} from "@mui/material/styles";
+import CssBaseline from "@mui/material/CssBaseline";
+import {AppBar, Box, Container, IconButton, Stack, Toolbar} from "@mui/material";
+
+function Header({drawerWidth, handleDrawerToggle}) {
+ const theme = useTheme();
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default Header;
diff --git a/src/components/layouts/Dashboard/Sidebar/SidebarDrawer.jsx b/src/components/layouts/Dashboard/Sidebar/SidebarDrawer.jsx
new file mode 100644
index 0000000..3f1d449
--- /dev/null
+++ b/src/components/layouts/Dashboard/Sidebar/SidebarDrawer.jsx
@@ -0,0 +1,27 @@
+import {Divider, Stack, Toolbar, Typography} from "@mui/material";
+import {useTranslations} from "next-intl";
+import SidebarList from "./SidebarList";
+import useUser from "@/lib/app/hooks/useUser";
+
+const SidebarDrawer = ({handleDrawerToggle}) => {
+ const {user} = useUser();
+ const t = useTranslations();
+ return (
+ <>
+
+
+
+ {t("app_short_name")}
+
+
+ {user.full_name} | {user.position}
+
+
+
+
+
+ >
+ );
+};
+
+export default SidebarDrawer;
diff --git a/src/components/layouts/Dashboard/Sidebar/SidebarList.jsx b/src/components/layouts/Dashboard/Sidebar/SidebarList.jsx
new file mode 100644
index 0000000..1e570f5
--- /dev/null
+++ b/src/components/layouts/Dashboard/Sidebar/SidebarList.jsx
@@ -0,0 +1,57 @@
+import {Divider, List} from "@mui/material";
+import {Fragment, useEffect, useReducer} from "react";
+import SidebarListItem from "./SidebarListItem";
+import sidebarMenu from "@/core/data/sidebarMenu";
+import {useRouter} from "next/router";
+import useUser from "@/lib/app/hooks/useUser";
+
+function reducer(state, action) {
+ switch (action.type) {
+ case "COLLAPSE_MENU":
+ return state.map((itemsArr) =>
+ itemsArr.map((item) =>
+ action.key === item.key ? { ...item, showSubItem: !item.showSubItem } : item
+ )
+ );
+ case "SELECTED":
+ return state.map((itemsArr) =>
+ itemsArr.map((item) => ({
+ ...item,
+ selected: item.type === "page" && action.route === item.route,
+ }))
+ );
+ default:
+ throw new Error();
+ }
+}
+
+export default function SidebarList({handleDrawerToggle}) {
+ const [itemMenu, dispatch] = useReducer(reducer, sidebarMenu);
+ const {user} = useUser();
+ const router = useRouter();
+
+ useEffect(() => {
+ dispatch({type: "SELECTED", route: router.pathname});
+ }, [router.pathname]);
+
+ return (
+
+ {itemMenu.map((itemArr, index) => (
+
+ {itemArr.map((item) =>
+
+ {(user?.permissions?.includes(item.permission) || item.permission === "all") &&
+ }
+
+ )}
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/layouts/Dashboard/Sidebar/SidebarListItem.jsx b/src/components/layouts/Dashboard/Sidebar/SidebarListItem.jsx
new file mode 100644
index 0000000..2195ed3
--- /dev/null
+++ b/src/components/layouts/Dashboard/Sidebar/SidebarListItem.jsx
@@ -0,0 +1,85 @@
+import {NextLinkComposed} from "@/core/components/LinkRouting";
+import ExpandLess from "@mui/icons-material/ExpandLess";
+import ExpandMore from "@mui/icons-material/ExpandMore";
+import {Badge, IconButton, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography,} from "@mui/material";
+import {useTranslations} from "next-intl";
+import {Fragment} from "react";
+import SidebarListSubItem from "./SidebarListSubItem";
+import useNotification from "@/lib/app/hooks/useNotification";
+
+const SidebarListItem = ({item, dispatch, handleDrawerToggle}) => {
+ const t = useTranslations();
+ const { notification_count } = useNotification();
+
+ const hasSubItems = item.type === "menu" && item.subItem && item.subItem.length > 0;
+
+ const renderBadge = () => (
+
+
+
+ );
+
+ return (
+ <>
+
+ {
+ if (hasSubItems) {
+ dispatch({ type: "COLLAPSE_MENU", key: item.key });
+ }
+ }}
+ sx={{
+ minHeight: 48,
+ }}
+
+ >
+
+ {item.icon}
+
+
+ {t(item.secondary)}
+
+ ) : null
+ }
+ />
+
+ {hasSubItems && (item.showSubItem ? : )}
+
+
+ {item.subItem && (
+
+ )}
+ >
+ );
+};
+
+export default SidebarListItem;
diff --git a/src/components/layouts/Dashboard/Sidebar/SidebarListSubItem.jsx b/src/components/layouts/Dashboard/Sidebar/SidebarListSubItem.jsx
new file mode 100644
index 0000000..1803b96
--- /dev/null
+++ b/src/components/layouts/Dashboard/Sidebar/SidebarListSubItem.jsx
@@ -0,0 +1,73 @@
+import {NextLinkComposed} from "@/core/components/LinkRouting";
+import {
+ Badge,
+ Collapse,
+ IconButton,
+ List,
+ ListItem,
+ ListItemButton,
+ ListItemIcon,
+ ListItemText,
+ Typography,
+} from "@mui/material";
+import {useTranslations} from "next-intl";
+import useNotification from "@/lib/app/hooks/useNotification";
+
+const SidebarListSubItem = ({item, handleDrawerToggle}) => {
+ const t = useTranslations();
+ const {notification_count} = useNotification();
+
+ const renderBadge = (subitem) => (
+
+
+
+ );
+
+ return (
+
+
+ {item.subItem.map((subitem) => (
+
+
+
+ {subitem.icon}
+
+
+ {t(subitem.secondary)}
+
+ ) : null
+ }/>
+
+
+ ))}
+
+
+ );
+};
+
+export default SidebarListSubItem;
diff --git a/src/components/layouts/Dashboard/Sidebar/__test__/SidebarDrawer.test.js b/src/components/layouts/Dashboard/Sidebar/__test__/SidebarDrawer.test.js
new file mode 100644
index 0000000..e095ffc
--- /dev/null
+++ b/src/components/layouts/Dashboard/Sidebar/__test__/SidebarDrawer.test.js
@@ -0,0 +1,23 @@
+import {render, screen, waitFor} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../mocks/AppWithProvider";
+import SidebarDrawer from "@/components/layouts/Dashboard/Sidebar/SidebarDrawer";
+
+describe('SidebarDrawer', () => {
+ describe('Rendering', () => {
+ it('renders system name in sidebar', async () => {
+ render();
+ await waitFor(()=>{
+ const systemName = screen.getByRole('heading', { level: 6 });
+ expect(systemName).toHaveTextContent('سامانه CRM');
+ })
+ });
+ it('renders user full name and position in sidebar', async () => {
+ localStorage.setItem("_token", 'token');
+ render();
+ await waitFor(()=>{
+ const userName = screen.queryByText(/Witel Company | Software Engineer/i);
+ expect(userName).toBeInTheDocument();
+ })
+ });
+ })
+})
diff --git a/src/components/layouts/Dashboard/Sidebar/__test__/SidebarList.test.js b/src/components/layouts/Dashboard/Sidebar/__test__/SidebarList.test.js
new file mode 100644
index 0000000..1897c9b
--- /dev/null
+++ b/src/components/layouts/Dashboard/Sidebar/__test__/SidebarList.test.js
@@ -0,0 +1,16 @@
+import {render, screen, waitFor} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../mocks/AppWithProvider";
+import SidebarList from "@/components/layouts/Dashboard/Sidebar/SidebarList";
+
+describe('SidebarList', () => {
+ describe('Rendering', () => {
+ it('renders sidebar items based on user permissions', async () => {
+ localStorage.setItem("_token", 'token');
+ render();
+ await waitFor(()=>{
+ const sidebarItem = screen.getByText('مدیریت نقش ها');
+ expect(sidebarItem).toBeInTheDocument();
+ })
+ });
+ })
+})
diff --git a/src/components/layouts/Dashboard/Sidebar/__test__/SidebarListItem.test.js b/src/components/layouts/Dashboard/Sidebar/__test__/SidebarListItem.test.js
new file mode 100644
index 0000000..18313c5
--- /dev/null
+++ b/src/components/layouts/Dashboard/Sidebar/__test__/SidebarListItem.test.js
@@ -0,0 +1,125 @@
+import AccessibilityIcon from "@mui/icons-material/Accessibility";
+import {fireEvent, render, screen} from "@testing-library/react";
+import SidebarListItem from "@/components/layouts/Dashboard/Sidebar/SidebarListItem";
+import MockAppWithProviders from "../../../../../../mocks/AppWithProvider";
+
+describe('SidebarListItemComponent', () => {
+ describe('Rendering', () => {
+ it('should show sidebarlistitem name in sidebar', () => {
+ const sampleItem={
+ key: "sidebar.role-management",
+ name : "role_management",
+ type: "page",
+ route: "/dashboard/role-management",
+ icon: ,
+ selected: false,
+ permission: "manage_roles",
+ }
+ render();
+ expect(screen.getByText('مدیریت نقش ها')).toBeInTheDocument();
+ });
+ it('should show sidebarlistitem icon in sidebar', () => {
+ const sampleItem={
+ key: "sidebar.role-management",
+ name : "role_management",
+ type: "page",
+ route: "/dashboard/role-management",
+ icon: ,
+ selected: false,
+ permission: "manage_roles",
+ }
+ render();
+ const iconElement = screen.getByLabelText("Sidebar Icon");
+ expect(iconElement).toBeInTheDocument();
+ });
+ it('should have selected class for subitem', () => {
+ const sampleItem={
+ key: "sidebar.role-management",
+ name : "role_management",
+ type: "page",
+ route: "/dashboard/role-management",
+ icon: ,
+ selected: true,
+ permission: "manage_roles",
+ }
+ render();
+ const element = screen.getByText('مدیریت نقش ها');
+ expect(element.parentElement.parentElement).toHaveClass('Mui-selected');
+ });
+
+ it('should not have selected class for subitem', () => {
+ const sampleItem={
+ key: "sidebar.role-management",
+ name : "role_management",
+ type: "menu",
+ route: "/dashboard/role-management",
+ icon: ,
+ selected: false,
+ permission: "manage_roles",
+ }
+ render();
+ const element = screen.getByText('مدیریت نقش ها');
+ expect(element.parentElement.parentElement).not.toHaveClass('Mui-selected');
+ });
+
+ it('should show sidebarlistitem secondary in sidebar', () => {
+ const sampleItem={
+ key: "sidebar.role-management",
+ name : "role_management",
+ secondary: "between",
+ type: "menu",
+ route: "/dashboard/role-management",
+ icon: ,
+ selected: false,
+ permission: "manage_roles",
+ }
+ render();
+ expect(screen.getByText('میان')).toBeInTheDocument();
+ });
+
+ it('should not show sidebarlistitem secondary in sidebar', () => {
+ const sampleItem={
+ key: "sidebar.role-management",
+ name : "role_management",
+ type: "menu",
+ route: "/dashboard/role-management",
+ icon: ,
+ selected: false,
+ permission: "manage_roles",
+ }
+ render();
+ expect(screen.queryByText('secondry')).not.toBeInTheDocument();
+ });
+ })
+ describe('Behavior', () => {
+ it('should call dispatch when hasSubItems is true and item is clicked', () => {
+ const dispatchMock = jest.fn();
+ const sampleItem={
+ key: "sidebar.role-management",
+ name : "role_management",
+ type: "menu",
+ route: "/dashboard/role-management",
+ icon: ,
+ selected: false,
+ permission: "manage_roles",
+ subItem :[
+ {
+ key: "greaterThan",
+ route: "/dashboard/role-management",
+ secondary: "between",
+ name : "sidebar.admin",
+ selected: true,
+ icon: ,
+ }
+ ]
+ }
+ render();
+ const listItem = screen.getByText('مدیریت نقش ها');
+ fireEvent.click(listItem);
+ expect(dispatchMock).toHaveBeenCalledWith({
+ type: 'COLLAPSE_MENU',
+ key: "sidebar.role-management",
+ });
+ });
+ })
+})
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/Sidebar/__test__/SidebarListSubitem.test.js b/src/components/layouts/Dashboard/Sidebar/__test__/SidebarListSubitem.test.js
new file mode 100644
index 0000000..9551937
--- /dev/null
+++ b/src/components/layouts/Dashboard/Sidebar/__test__/SidebarListSubitem.test.js
@@ -0,0 +1,103 @@
+import {render, screen} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../mocks/AppWithProvider";
+import SidebarListSubItem from "@/components/layouts/Dashboard/Sidebar/SidebarListSubItem";
+import AccessibilityIcon from "@mui/icons-material/Accessibility";
+
+describe('SidebarListSubItemComponent', () => {
+ describe('Rendering', () => {
+ const sampleItem={
+ showSubItem:true,
+ key: "sidebar.role-management",
+ name : "role_management",
+ type: "menu",
+ route: "/dashboard/role-management",
+ icon: ,
+ selected: false,
+ permission: "manage_roles",
+ subItem :[
+ {
+ key: "greaterThan",
+ route: "/dashboard/role-management",
+ secondary: "between",
+ name : "sidebar.admin",
+ selected: true,
+ icon: ,
+ }
+ ]
+ }
+ it('should show subitem name in sidebar', () => {
+ render();
+ expect(screen.getByText('بزرگتر از')).toBeInTheDocument();
+
+ });
+ it('should not show subitem name in sidebar', () => {
+ const sampleItem={
+ showSubItem:false,
+ key: "sidebar.role-management",
+ name : "role_management",
+ type: "menu",
+ route: "/dashboard/role-management",
+ icon: ,
+ selected: false,
+ permission: "manage_roles",
+ subItem :[
+ {
+ key: "greaterThan",
+ route: "/dashboard/role-management",
+ secondary: "between",
+ name : "sidebar.admin",
+ selected: false,
+ icon: ,
+ }
+ ]
+ }
+ render();
+ expect(screen.queryByText('مدیریت نقش ها')).not.toBeInTheDocument();
+ });
+ it('should show subitem secondary in sidebar', () => {
+ render();
+ expect(screen.getByText('میان')).toBeInTheDocument();
+ });
+
+ it('should not show subitem secondary in sidebar', () => {
+ render();
+ expect(screen.queryByText('secondry')).not.toBeInTheDocument();
+ });
+ it('should have selected class for subitem', () => {
+ render();
+ const parentElement = screen.queryByText('بزرگتر از').parentElement.parentElement;
+ expect(parentElement).toHaveClass('Mui-selected');
+ });
+
+ it('should not have selected class for subitem', () => {
+ const sampleItem={
+ showSubItem:true,
+ key: "sidebar.role-management",
+ name : "role_management",
+ type: "menu",
+ route: "/dashboard/role-management",
+ icon: ,
+ selected: false,
+ permission: "manage_roles",
+ subItem :[
+ {
+ key: "greaterThan",
+ route: "/dashboard/role-management",
+ secondary: "between",
+ name : "sidebar.admin",
+ selected: false,
+ icon: ,
+ }
+ ]
+ }
+ render();
+ const parentElement = screen.queryByText('بزرگتر از').parentElement.parentElement;
+ expect(parentElement).not.toHaveClass('Mui-selected');
+ });
+ it('should show subitem icon', () => {
+ render();
+ const iconElement = screen.getByLabelText("Sidebar Icon");
+ expect(iconElement).toBeInTheDocument();
+ });
+ })
+})
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/Sidebar/__test__/index.test.js b/src/components/layouts/Dashboard/Sidebar/__test__/index.test.js
new file mode 100644
index 0000000..740c384
--- /dev/null
+++ b/src/components/layouts/Dashboard/Sidebar/__test__/index.test.js
@@ -0,0 +1,17 @@
+import {render, screen, act} from "@testing-library/react";
+import MockAppWithProviders from "../../../../../../mocks/AppWithProvider";
+import Sidebar from "@/components/layouts/Dashboard/Sidebar";
+
+
+describe('Sidebar', () => {
+ describe('Rendering', () => {
+ it('Mobile Drawer is displayed when mobileOpen is true', () => {
+ const props={
+ mobileOpen: true,
+ };
+ render();
+ const mobileDrawer = screen.getByTestId('mobile-drawer');
+ expect(mobileDrawer).toBeVisible();
+ });
+ })
+})
diff --git a/src/components/layouts/Dashboard/Sidebar/index.jsx b/src/components/layouts/Dashboard/Sidebar/index.jsx
new file mode 100644
index 0000000..c0b6d6d
--- /dev/null
+++ b/src/components/layouts/Dashboard/Sidebar/index.jsx
@@ -0,0 +1,47 @@
+import {Box, Drawer} from "@mui/material";
+import SidebarDrawer from "./SidebarDrawer";
+
+const Sidebar = (props) => {
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Sidebar;
diff --git a/src/components/layouts/Dashboard/index.jsx b/src/components/layouts/Dashboard/index.jsx
new file mode 100644
index 0000000..3ee24c6
--- /dev/null
+++ b/src/components/layouts/Dashboard/index.jsx
@@ -0,0 +1,45 @@
+import Header from "src/components/layouts/Dashboard/Header";
+import Sidebar from "src/components/layouts/Dashboard/Sidebar";
+import FullPageLayout from "@/layouts/FullPageLayout";
+import {Toolbar} from "@mui/material";
+import BreadCrumbs from "src/components/layouts/Dashboard/Breadcrumbs";
+import {useState} from "react";
+
+const drawerWidth = 240;
+
+const Dashboard = (props) => {
+ const {window} = props;
+ const [mobileOpen, setMobileOpen] = useState(false);
+ const container =
+ window !== undefined ? () => window().document.body : undefined;
+
+ const handleDrawerToggle = () => {
+ setMobileOpen(!mobileOpen);
+ };
+ return (
+
+
+
+
+
+
+
+ {props.children}
+
+
+
+ )
+}
+
+export default Dashboard
\ No newline at end of file
diff --git a/src/components/login-expert/LoginExpertComponent.jsx b/src/components/login-expert/LoginExpertComponent.jsx
new file mode 100644
index 0000000..d2a2d13
--- /dev/null
+++ b/src/components/login-expert/LoginExpertComponent.jsx
@@ -0,0 +1,123 @@
+import {Box, Button, Container, Paper, Stack, TextField, Typography} from "@mui/material";
+import {Field, Formik} from "formik";
+import SvgLogin from "@/core/components/svgs/SvgLogin";
+import StyledForm from "@/core/components/StyledForm";
+import PasswordField from "@/core/components/PasswordField";
+import LoginIcon from "@mui/icons-material/Login";
+import {GET_USER_TOKEN} from "@/core/data/apiRoutes";
+import * as Yup from "yup";
+import useRequest from "@/lib/app/hooks/useRequest";
+import useUser from "@/lib/app/hooks/useUser";
+import {useTranslations} from "next-intl";
+
+const LoginExpertComponent = () => {
+ const t = useTranslations();
+ const requestServer = useRequest()
+ const {setToken} = useUser(); // pass token to set token
+ const handleSubmit = (values, props) => {
+ requestServer(GET_USER_TOKEN, 'post', {
+ data: {
+ username: values.username,
+ password: values.password,
+ },
+ success: {
+ notification: {show: false}
+ }
+ }).then((response) => {
+ setToken(response.data.data.token)
+ }).catch(() => {
+ props.setSubmitting(false)
+ })
+ };
+ const initialValues = {
+ username: "",
+ password: "",
+ };
+ const validationSchema = Yup.object().shape({
+ username: Yup.string().required(t("LoginPage.username_error_message_required")),
+ password: Yup.string().required(t("LoginPage.password_error_message_required")),
+ });
+
+ return (
+
+
+
+ {(props) => (
+
+
+
+
+
+ {t("login_expert")}
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ )
+}
+export default LoginExpertComponent
\ No newline at end of file
diff --git a/src/components/login-expert/LoginLinkRouting.jsx b/src/components/login-expert/LoginLinkRouting.jsx
new file mode 100644
index 0000000..1930b3c
--- /dev/null
+++ b/src/components/login-expert/LoginLinkRouting.jsx
@@ -0,0 +1,28 @@
+import LinkRouting from "@/core/components/LinkRouting";
+import {Stack} from "@mui/material";
+import {useRouter} from "next/router";
+import {useTranslations} from "next-intl";
+
+const LoginLinkRouting = () => {
+ const t = useTranslations();
+ const router = useRouter()
+ const backUrlDecodedPath = router.query?.back_url
+
+ return (
+
+
+ {t("LoginPage.link_routing_back_to")}{" "}
+ {backUrlDecodedPath
+ ? t("LoginPage.link_routing_previuos_page")
+ : t("LoginPage.link_routing_main_page")}
+
+
+ )
+}
+export default LoginLinkRouting
\ No newline at end of file
diff --git a/src/components/login-expert/__test__/LoginExpertComponent.test.js b/src/components/login-expert/__test__/LoginExpertComponent.test.js
new file mode 100644
index 0000000..695dbcb
--- /dev/null
+++ b/src/components/login-expert/__test__/LoginExpertComponent.test.js
@@ -0,0 +1,154 @@
+import {act, findByText, fireEvent, getByText, render, screen, waitFor} from '@testing-library/react';
+import MockAppWithProviders from '../../../../mocks/AppWithProvider';
+import mockRouter from 'next-router-mock'
+import LoginExpertComponent from "@/components/login-expert/LoginExpertComponent";
+import LoginLinkRouting from "@/components/login-expert/LoginLinkRouting";
+
+describe('Login expert component from login page', () => {
+
+ describe('Rendering', () => {
+ it('LoginExpertComponent should see login expert text on the page',() => {
+ render(
+
+
+
+ );
+ const loginExpertElement = screen.getByRole('heading', { level: 4 });
+ expect(loginExpertElement).toHaveTextContent('ورود کارشناس');
+ });
+
+ it('LoginExpertComponent from LoginComponent Button should render login text', () => {
+ render(
+
+
+
+ );
+ const button = screen.getByText("ورود");
+ // Assertions for button props
+ expect(button).toHaveAttribute('type', 'submit');
+ expect(button).toHaveTextContent('ورود');
+ });
+
+ it("Show error when username is empty", async ()=>{
+ render(
+
+
+
+ );
+
+ const usernameInput = screen.getByLabelText("نام کاربری")
+
+ expect(screen.queryByText("وارد کردن نام کاربری الزامیست")).not.toBeInTheDocument()
+
+ fireEvent.blur(usernameInput);
+
+ await waitFor(()=>{
+ expect(screen.queryByText("وارد کردن نام کاربری الزامیست")).toBeInTheDocument()
+ })
+
+ fireEvent.change(usernameInput, {target : {value : "testuser"}})
+
+ await waitFor(()=>{
+ expect(screen.queryByText("وارد کردن نام کاربری الزامیست")).not.toBeInTheDocument()
+ })
+ })
+ it("Show error when password is empty", async ()=>{
+ render(
+
+
+
+ );
+
+ const passwordInput = screen.getByLabelText("رمز عبور")
+
+ expect(screen.queryByText("وارد کردن رمز عبور الزامیست")).not.toBeInTheDocument()
+
+ fireEvent.blur(passwordInput);
+
+ await waitFor(()=>{
+ expect(screen.queryByText("وارد کردن رمز عبور الزامیست")).toBeInTheDocument()
+ })
+
+ fireEvent.change(passwordInput, {target : {value : "testuser"}})
+
+ await waitFor(()=>{
+ expect(screen.queryByText("وارد کردن رمز عبور الزامیست")).not.toBeInTheDocument()
+ })
+ })
+ });
+
+ describe("Behavior", ()=>{
+ it('Should fill the username field', async () => {
+ render(
+
+
+
+ );
+ // Get username and password input elements by their name attributes
+ const usernameInput = screen.getByLabelText('نام کاربری');
+
+ fireEvent.change(usernameInput, {target: {value: 'testuser'}});
+ await act(async ()=>{
+ // Simulate user input
+ expect(usernameInput).toHaveValue('testuser');
+ })
+ });
+
+ it('Should fill the password fields', async () => {
+ render(
+
+
+
+ );
+ // Get username and password input elements by their name attributes
+ const passwordInput = screen.getByLabelText('رمز عبور');
+ fireEvent.change(passwordInput, {target: {value: 'password123'}});
+
+ await act(async ()=>{
+ // Simulate user input
+ expect(passwordInput).toHaveValue('password123');
+ })
+ });
+
+ // this test is for when back url does not exist to run this test you should comment if in mock func
+ it('Should get main page when back url dos not exist', async () => {
+ render(
+
+
+
+ )
+ const linkElement = await screen.findByTestId("link_routing")
+ expect(linkElement).toHaveTextContent("بازگشت به صفحه اصلی")
+ });
+
+ })
+ describe("validation", ()=>{
+ it("Disabled submit button until fields completed", async ()=> {
+
+ render(
+
+
+
+ )
+ const usernameInput = screen.getByLabelText('نام کاربری');
+ const passwordInput = screen.getByLabelText('رمز عبور');
+ const submitButton = screen.getByText('ورود');
+
+ expect(submitButton).toBeDisabled();
+
+ fireEvent.change(usernameInput, {target: {value: 'testuser'}});
+
+ await waitFor(async () => {
+ // Now, the submit button should be enabled because both fields are completed
+ expect(submitButton).toBeDisabled();
+ });
+
+ fireEvent.change(passwordInput, {target: {value: 'password123'}});
+
+ await waitFor(async () => {
+ // Now, the submit button should be enabled because both fields are completed
+ expect(submitButton).toBeEnabled();
+ });
+ })
+ })
+});
\ No newline at end of file
diff --git a/src/components/login-expert/__test__/LoginLinkRouting.test.js b/src/components/login-expert/__test__/LoginLinkRouting.test.js
new file mode 100644
index 0000000..a63d314
--- /dev/null
+++ b/src/components/login-expert/__test__/LoginLinkRouting.test.js
@@ -0,0 +1,18 @@
+import {render, screen} from "@testing-library/react";
+import MockAppWithProviders from "../../../../mocks/AppWithProvider";
+import LoginLinkRouting from "@/components/login-expert/LoginLinkRouting";
+
+describe("Login expert component from login page",()=>{
+
+ describe('Rendering', () => {
+ it('LoginLinkRouting from LoginComponent Should get main page when back url dos not exist', async () => {
+ render(
+
+
+
+ )
+ const linkElement = await screen.findByTestId("link_routing")
+ expect(linkElement).toHaveTextContent("بازگشت به صفحه اصلی")
+ });
+ })
+})
diff --git a/src/components/login-expert/index.jsx b/src/components/login-expert/index.jsx
new file mode 100644
index 0000000..dc36d9a
--- /dev/null
+++ b/src/components/login-expert/index.jsx
@@ -0,0 +1,18 @@
+import CenterLayout from "@/layouts/CenterLayout";
+import FullPageLayout from "@/layouts/FullPageLayout";
+import LoginLinkRouting from "@/components/login-expert/LoginLinkRouting";
+import LoginExpertComponent from "@/components/login-expert/LoginExpertComponent";
+
+const LoginComponent = () => {
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default LoginComponent;
diff --git a/src/core/components/AvatarUpload.jsx b/src/core/components/AvatarUpload.jsx
new file mode 100644
index 0000000..03a9cba
--- /dev/null
+++ b/src/core/components/AvatarUpload.jsx
@@ -0,0 +1,128 @@
+import {Avatar, Box, TextField} from "@mui/material";
+import {useState} from "react";
+import DeleteIcon from "@mui/icons-material/Delete";
+import AddIcon from "@mui/icons-material/Add";
+
+const AvatarUpload = ({user, setFieldValue, valueAvatar, changeFlag}) => {
+ const [selectedImage, setSelectedImage] = useState(user.expert_avatar);
+ const [isHovered, setIsHovered] = useState(false);
+
+ const handleImageChange = (event) => {
+ const newImage = event.target?.files?.[0];
+ if (newImage) {
+ setSelectedImage(URL.createObjectURL(newImage));
+ setFieldValue(valueAvatar, newImage);
+ setFieldValue(changeFlag, true);
+ } else {
+ setSelectedImage("");
+ setFieldValue(valueAvatar, null);
+ }
+ };
+
+ const handleDeleteImage = () => {
+ setSelectedImage("");
+ setFieldValue(valueAvatar, null);
+ setFieldValue(changeFlag, true);
+ };
+
+ const handleMouseEnter = () => {
+ setIsHovered(true);
+ };
+
+ const handleMouseLeave = () => {
+ setIsHovered(false);
+ };
+
+ return (
+
+
+
+
+ {isHovered && (
+
+ {selectedImage ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+
+
+ );
+};
+
+export default AvatarUpload;
diff --git a/src/core/components/DataTable.jsx b/src/core/components/DataTable.jsx
new file mode 100644
index 0000000..41bdbaa
--- /dev/null
+++ b/src/core/components/DataTable.jsx
@@ -0,0 +1,172 @@
+import {useTranslations} from "next-intl";
+import useLanguage from "@/lib/app/hooks/useLanguage";
+import {useEffect, useMemo, useState} from "react";
+import moment from "moment-jalaali";
+import useSWR from "swr";
+import {Typography} from "@mui/material";
+import MaterialReactTable from "material-react-table";
+import useRequest from "@/lib/app/hooks/useRequest";
+
+function DataTable(props) {
+ const requestServer = useRequest({auth: true})
+ const t = useTranslations();
+ const {languageApp, languageList} = useLanguage();
+ const [columnFilters, setColumnFilters] = useState([]);
+ const [sorting, setSorting] = useState([]);
+ const [pagination, setPagination] = useState({pageIndex: 0, pageSize: 10});
+ const [columnFilterFns, setColumnFilterFns] = useState(() => {
+ let output = {};
+ const list = props.columns.map((item) =>
+ item.enableColumnFilter ? {[item.id]: item.filterFn} : {[item.id]: ""}
+ );
+ for (const key in list) {
+ const nestedObj = list[key];
+ for (const nestedKey in nestedObj) {
+ output[nestedKey] = nestedObj[nestedKey];
+ }
+ }
+ return output;
+ });
+
+ const [updateTime, setUpdateTime] = useState(
+ moment().format("HH:mm | jYYYY/jMM/jDD")
+ );
+
+ const tableLocalization = useMemo(
+ () =>
+ languageList.find((item) => item.key === languageApp).tableLocalization,
+ [languageApp, languageList]
+ );
+
+ const fetchUrl = useMemo(() => {
+ const params = new URLSearchParams();
+ params.set(
+ "start",
+ `${pagination.pageIndex * pagination.pageSize}`
+ );
+ const filters = columnFilters.map((filter) => {
+ let datatype;
+ for (const i in props.columns) {
+ if (props.columns[i].id === filter.id) {
+ datatype = props.columns[i].datatype;
+ }
+ }
+ return {
+ ...filter,
+ fn: columnFilterFns[filter.id],
+ datatype: datatype,
+ };
+ });
+ params.set("size", pagination.pageSize);
+ params.set("filters", JSON.stringify(filters ?? []));
+ params.set("sorting", JSON.stringify(sorting ?? []));
+ return `${props.tableUrl}?${params}`;
+ }, [
+ props.tableUrl,
+ columnFilters,
+ columnFilterFns,
+ pagination,
+ sorting,
+ props.columns,
+ ]);
+
+ const {data, isValidating, mutate} = useSWR(fetchUrl, (...args) =>
+ requestServer(args, 'get', {
+ pending: false,
+ success: {notification: {show: false}}
+ }).then((response) => response.data).catch(() => {
+ })
+ , {
+ revalidateIfStale: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: true,
+ keepPreviousData: true
+ });
+
+ useEffect(() => {
+ setUpdateTime(moment().format("HH:mm | jYYYY/jMM/jDD"));
+ }, [isValidating, languageApp]);
+
+ return (
+ (
+ <>
+ {props.enableCustomToolbar /* send condition */
+ ? /* send component */
+ : ""}
+ >
+ )}
+ renderBottomToolbarCustomActions={({table}) => (
+ <>
+ {props.enableLastUpdate /* send condition */ ? (
+
+ {t("last_updated_at")}: {updateTime}
+
+ ) : (
+ ""
+ )}
+ >
+ )}
+ state={{
+ showProgressBars: isValidating,
+ columnFilters,
+ columnFilterFns,
+ pagination,
+ sorting,
+ }}
+ positionActionsColumn={"last"}
+ enableRowActions={props.enableRowActions}
+ renderRowActions={({row}) => }
+ {...props}
+ />
+ );
+}
+
+export default DataTable;
\ No newline at end of file
diff --git a/src/core/components/DialogTransition.jsx b/src/core/components/DialogTransition.jsx
new file mode 100644
index 0000000..d94765f
--- /dev/null
+++ b/src/core/components/DialogTransition.jsx
@@ -0,0 +1,6 @@
+import {forwardRef} from "react";
+import {Slide} from "@mui/material";
+
+export const DialogTransition = forwardRef(function DialogTransition(props, ref) {
+ return ;
+});
diff --git a/src/core/components/GlobalHead.jsx b/src/core/components/GlobalHead.jsx
new file mode 100644
index 0000000..5d6186d
--- /dev/null
+++ b/src/core/components/GlobalHead.jsx
@@ -0,0 +1,58 @@
+import Head from "next/head";
+
+const GlobalHead = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default GlobalHead
\ No newline at end of file
diff --git a/src/core/components/ImageConvertor.jsx b/src/core/components/ImageConvertor.jsx
new file mode 100644
index 0000000..9f99bc1
--- /dev/null
+++ b/src/core/components/ImageConvertor.jsx
@@ -0,0 +1,22 @@
+import ImageResize from "image-resize";
+
+const ImageResizer = async (image) => {
+ const imageResize = new ImageResize({
+ quality: 1,
+ format: "jpg",
+ outputType: "base64",
+ width: 400,
+ });
+
+ const get = await imageResize.get(image);
+ const resize = await imageResize.resize(get).then();
+ const output = await imageResize.output(resize).then();
+
+ const avatar = await (await import("image-to-file-converter"))
+ .base64ToFile(output)
+ .then();
+
+ return avatar;
+};
+
+export default ImageResizer;
diff --git a/src/core/components/LinkRouting.jsx b/src/core/components/LinkRouting.jsx
new file mode 100644
index 0000000..f8fac0a
--- /dev/null
+++ b/src/core/components/LinkRouting.jsx
@@ -0,0 +1,116 @@
+import MuiLink from "@mui/material/Link";
+import {styled} from "@mui/material/styles";
+import clsx from "clsx";
+import NextLink from "next/link";
+import {useRouter} from "next/router";
+import * as React from "react";
+
+// Add support for the sx prop for consistency with the other branches.
+const Anchor = styled("a")({});
+
+export const NextLinkComposed = React.forwardRef(function NextLinkComposed(
+ props,
+ ref
+) {
+ const {
+ to,
+ linkAs,
+ replace,
+ scroll,
+ shallow,
+ prefetch,
+ legacyBehavior = true,
+ locale,
+ ...other
+ } = props;
+
+ return (
+
+
+
+ );
+});
+
+// A styled version of the Next.js Link component:
+// https://nextjs.org/docs/api-reference/next/link
+const LinkRouting = React.forwardRef(function Link(props, ref) {
+ const {
+ activeClassName = "active",
+ as,
+ className: classNameProps,
+ href,
+ legacyBehavior,
+ linkAs: linkAsProp,
+ locale,
+ noLinkStyle,
+ prefetch,
+ replace,
+ role, // Link don't have roles.
+ scroll,
+ shallow,
+ ...other
+ } = props;
+
+ const router = useRouter();
+ const pathname = typeof href === "string" ? href : href.pathname;
+ const className = clsx(classNameProps, {
+ [activeClassName]: router.pathname === pathname && activeClassName,
+ });
+
+ const isExternal =
+ typeof href === "string" &&
+ (href.indexOf("http") === 0 || href.indexOf("mailto:") === 0);
+
+ if (isExternal) {
+ if (noLinkStyle) {
+ return ;
+ }
+
+ return ;
+ }
+
+ const linkAs = linkAsProp || as;
+ const nextjsProps = {
+ to: href,
+ linkAs,
+ replace,
+ scroll,
+ shallow,
+ prefetch,
+ legacyBehavior,
+ locale,
+ };
+
+ if (noLinkStyle) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+});
+
+export default LinkRouting;
diff --git a/src/core/components/LoadingHardPage.jsx b/src/core/components/LoadingHardPage.jsx
new file mode 100644
index 0000000..5d00cd0
--- /dev/null
+++ b/src/core/components/LoadingHardPage.jsx
@@ -0,0 +1,43 @@
+import SvgLoading from "@/core/components/svgs/SvgLoading";
+import {Backdrop, Box} from "@mui/material";
+import {styled} from "@mui/material/styles";
+
+const LoadingImage = styled(Box)({
+ "@keyframes load": {
+ "0%": {
+ // opacity: 0,
+ transform: "scale(1)",
+ },
+ "50%": {
+ // opacity: 1,
+ transform: "scale(2)",
+ },
+ "100%": {
+ // opacity: 0,
+ transform: "scale(1)",
+ },
+ },
+ animation: "load 2s infinite",
+});
+
+const LoadingHardPage = ({children, loading}) => {
+ return (
+ <>
+ theme.zIndex.modal + 1}}
+ open={loading}
+ >
+
+
+
+
+ {children}
+ >
+ );
+};
+
+export default LoadingHardPage;
diff --git a/src/core/components/Messages.jsx b/src/core/components/Messages.jsx
new file mode 100644
index 0000000..8a5cfc9
--- /dev/null
+++ b/src/core/components/Messages.jsx
@@ -0,0 +1,18 @@
+import CenterLayout from "@/layouts/CenterLayout";
+import FullPageLayout from "@/layouts/FullPageLayout";
+import SvgLoading from "@/core/components/svgs/SvgLoading";
+
+const Message = ({text, actions}) => {
+ return (
+
+
+
+ {text}
+ {actions}
+
+
+ );
+};
+
+export default Message;
diff --git a/src/core/components/Middleware/RolePermissionComponent.jsx b/src/core/components/Middleware/RolePermissionComponent.jsx
new file mode 100644
index 0000000..869f78a
--- /dev/null
+++ b/src/core/components/Middleware/RolePermissionComponent.jsx
@@ -0,0 +1,33 @@
+import { Button, Typography } from "@mui/material";
+import { useTranslations } from "next-intl";
+import { NextLinkComposed } from "@/core/components/LinkRouting";
+import Message from "@/core/components/Messages";
+
+const RolePermissionComponent = () => {
+ const t = useTranslations();
+
+ return (
+
+ {t("Permission.typography_you_dont_have_access")}
+
+ }
+ actions={
+ <>
+
+ >
+ }
+ />
+ );
+};
+
+export default RolePermissionComponent;
diff --git a/src/core/components/Middleware/WithAuthComponent.jsx b/src/core/components/Middleware/WithAuthComponent.jsx
new file mode 100644
index 0000000..9a6ff5d
--- /dev/null
+++ b/src/core/components/Middleware/WithAuthComponent.jsx
@@ -0,0 +1,36 @@
+import { Button, Typography } from "@mui/material";
+import { useRouter } from "next/router";
+import Message from "@/core/components/Messages";
+import { NextLinkComposed } from "@/core/components/LinkRouting";
+import {useTranslations} from "next-intl";
+
+const WithAuthComponent = () => {
+ const router = useRouter();
+ const t = useTranslations();
+
+ return (
+
+ {t("Authorization.typography_your_access_to_this_page_has_expired_Please_login_again")}
+
+ }
+ actions={
+ <>
+
+ >
+ }
+ />
+ );
+};
+
+export default WithAuthComponent;
diff --git a/src/core/components/Middleware/WithoutAuthComponent.jsx b/src/core/components/Middleware/WithoutAuthComponent.jsx
new file mode 100644
index 0000000..7dbb38d
--- /dev/null
+++ b/src/core/components/Middleware/WithoutAuthComponent.jsx
@@ -0,0 +1,25 @@
+import {Button, Stack, Typography} from "@mui/material";
+import Message from "@/core/components/Messages";
+import {useTranslations} from "next-intl";
+const WithoutAuthComponent = ({ backUrlDecodedPath }) => {
+ const t = useTranslations();
+
+ return (
+
+
+ {t("Authorization.typography_your_login_is_valid_and_you_do_not_need_to_login_again")}
+
+
+ {t("Authorization.typography_redirect_to")}{" "}
+ {backUrlDecodedPath
+ ? t("Authorization.typography_routing_previuos_page")
+ : t("Authorization.typography_routing_dashbaord_page")}
+
+
+ }
+ />
+ );
+};
+export default WithoutAuthComponent;
diff --git a/src/core/components/Middleware/__test__/RolePermissionComponent.test.js b/src/core/components/Middleware/__test__/RolePermissionComponent.test.js
new file mode 100644
index 0000000..44a583d
--- /dev/null
+++ b/src/core/components/Middleware/__test__/RolePermissionComponent.test.js
@@ -0,0 +1,23 @@
+import { render, screen } from '@testing-library/react';
+import MockAppWithProviders from "../../../../../mocks/AppWithProvider";
+import RolePermissionComponent from "@/core/components/Middleware/RolePermissionComponent";
+
+describe('RolePermissionComponent From RolePermissionComponent page', () => {
+ describe('Rendering', () => {
+ it('should render the error message when user dont have permission', () => {
+ render();
+
+ expect(screen.queryByText('شما دسترسی لازم برای مشاهده این صفحه را ندارید!')).toBeInTheDocument();
+
+ });
+
+ it('should render the back dashboard button when user dont have permission', () => {
+ render();
+
+ const backButton = screen.queryByText('بازگشت به صفحه اصلی');
+
+ expect(backButton).toBeInTheDocument();
+
+ });
+ });
+});
diff --git a/src/core/components/Middleware/__test__/WithAuthComponent.test.js b/src/core/components/Middleware/__test__/WithAuthComponent.test.js
new file mode 100644
index 0000000..a43e20f
--- /dev/null
+++ b/src/core/components/Middleware/__test__/WithAuthComponent.test.js
@@ -0,0 +1,39 @@
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import MockAppWithProviders from "../../../../../mocks/AppWithProvider";
+import WithAuthComponent from "@/core/components/Middleware/WithAuthComponent";
+import mockRouter from 'next-router-mock';
+
+describe('WithAuthComponent From WithAuthMiddleware', () => {
+ describe('Rendering', () => {
+ it('should see expired text in render component', () => {
+ render();
+
+ const authText = screen.queryByText('دسترسی شما منقضی شده است لطفا دوباره ورود نمایید.');
+
+ expect(authText).toBeInTheDocument();
+ });
+
+ it('should see login button in render component', () => {
+ render();
+
+ const loginButton = screen.queryByText('ورود');
+
+ expect(loginButton).toBeInTheDocument();
+ });
+ });
+ describe('behavior', () => {
+ it('should see if router updated in login button', () => {
+
+ mockRouter.query.back_url = 'back_url'
+ render();
+
+ const loginButton = screen.queryByText('ورود');
+
+ fireEvent.click(loginButton);
+
+ expect(mockRouter).toMatchObject({
+ query: { back_url: "back_url" },
+ });
+ });
+ });
+});
diff --git a/src/core/components/Middleware/__test__/WithoutAuthComponent.test.js b/src/core/components/Middleware/__test__/WithoutAuthComponent.test.js
new file mode 100644
index 0000000..520d7bd
--- /dev/null
+++ b/src/core/components/Middleware/__test__/WithoutAuthComponent.test.js
@@ -0,0 +1,37 @@
+import {render, screen, waitFor} from '@testing-library/react';
+import WithoutAuthComponent from "@/core/components/Middleware/WithoutAuthComponent";
+import MockAppWithProviders from "../../../../../mocks/AppWithProvider";
+import WithoutAuthMiddleware from "@/middlewares/WithoutAuth";
+
+describe('WithoutAuthComponent From WithoutAuthMiddleware', () => {
+ describe('Rendering', () => {
+ it('should render the message with correct text when backUrlDecodedPath is not provided', async () => {
+
+ localStorage.setItem("_token", 'token');
+
+ render();
+
+ await waitFor(()=>{
+ expect(screen.queryByText('شما دسترسی لازم به این صفحه را دارید نیاز به ورود مجدد نیست.')).toBeInTheDocument();
+
+ expect(screen.queryByText('درحال رفتن به صفحه داشبورد...')).toBeInTheDocument();
+ })
+
+ });
+
+ it('should render the message with a different text when backUrlDecodedPath is provided', async() => {
+ render();
+
+ localStorage.setItem("_token", 'token');
+
+ render();
+
+ await waitFor(()=>{
+
+ expect(screen.queryByText('درحال رفتن به صفحه قبل...')).toBeInTheDocument();
+ })
+
+
+ });
+ });
+});
diff --git a/src/core/components/MuiDatePicker.jsx b/src/core/components/MuiDatePicker.jsx
new file mode 100644
index 0000000..18be3cc
--- /dev/null
+++ b/src/core/components/MuiDatePicker.jsx
@@ -0,0 +1,61 @@
+import {LocalizationProvider, MobileDateTimePicker} from "@mui/x-date-pickers";
+import {AdapterDateFnsJalali} from "@mui/x-date-pickers/AdapterDateFnsJalali";
+import moment from "jalali-moment";
+import {faIR} from "@mui/x-date-pickers/locales";
+import {Box, IconButton} from "@mui/material";
+import ClearIcon from "@mui/icons-material/Clear";
+import {useTranslations} from "next-intl";
+
+export default function MuiDatePicker({column}) {
+ const t = useTranslations();
+ const filterFnValue = column.columnDef._filterFn;
+
+ return (
+
+
+ {
+ const date = new Date(newValue);
+ const formattedDate = moment(date)
+ .locale("en")
+ .format("YYYY-MM-DD HH:mm");
+ column.setFilterValue(formattedDate);
+ }}
+ slotProps={{
+ textField: {
+ placeholder: "تاریخ خود را وارد کنید",
+ helperText: `${t("filter_mode")}: ${t(filterFnValue)}`,
+ sx: {minWidth: "120px"},
+ variant: "standard",
+ },
+ }}
+ value={
+ column.getFilterValue()
+ ? new Date(column.getFilterValue())
+ : null
+ }
+ />
+
+ {
+ column.setFilterValue(null);
+ }}
+ sx={{
+ color: column.getFilterValue()
+ ? "rgba(0, 0, 0, 0.54)"
+ : "#bfbfbf",
+ }}
+ >
+
+
+
+ );
+}
diff --git a/src/core/components/NetworkComponent.jsx b/src/core/components/NetworkComponent.jsx
new file mode 100644
index 0000000..22d4620
--- /dev/null
+++ b/src/core/components/NetworkComponent.jsx
@@ -0,0 +1,33 @@
+import useNetwork from "@/lib/app/hooks/useNetwork";
+import {useEffect, useRef} from "react";
+import {toast} from "react-toastify";
+import WifiIcon from '@mui/icons-material/Wifi';
+import WifiOffIcon from '@mui/icons-material/WifiOff';
+import {useTranslations} from "next-intl";
+
+const NetworkComponent = () => {
+ const networkToastId = useRef(null);
+ const network = useNetwork()
+ const t = useTranslations()
+
+ useEffect(() => {
+ if (network.online) {
+ toast.update(networkToastId.current, {
+ type: toast.TYPE.SUCCESS,
+ render: t('online_message'),
+ autoClose: 2000,
+ closeButton: true,
+ closeOnClick: true,
+ icon:
+ });
+ return
+ }
+ networkToastId.current = toast.warn(t('offline_message'), {
+ autoClose: false, closeButton: false, closeOnClick: false, icon:
+ })
+ }, [network.online]);
+
+ return ''
+}
+
+export default NetworkComponent
\ No newline at end of file
diff --git a/src/core/components/NoSsrHandler.jsx b/src/core/components/NoSsrHandler.jsx
new file mode 100644
index 0000000..5a4c4e1
--- /dev/null
+++ b/src/core/components/NoSsrHandler.jsx
@@ -0,0 +1,8 @@
+import {NoSsr} from "@mui/material";
+
+const NoSsrHandler = ({isBot, children}) => {
+ if (isBot) return children;
+ return {children};
+};
+
+export default NoSsrHandler;
diff --git a/src/core/components/PasswordField/__test__/index.test.js b/src/core/components/PasswordField/__test__/index.test.js
new file mode 100644
index 0000000..479f91f
--- /dev/null
+++ b/src/core/components/PasswordField/__test__/index.test.js
@@ -0,0 +1,31 @@
+import {fireEvent, getByRole, render, screen} from "@testing-library/react";
+import PasswordField from "@/core/components/PasswordField";
+import {Formik} from "formik";
+
+describe('Password Field component in core folder', () => {
+ describe('Rendering', () => {
+ it('Show password when visibility icon clicked',() => {
+ render (
+ {}}>
+ {(props)=>(
+
+ )}
+
+ )
+
+ const iconElement = screen.getByLabelText("رمز عبور")
+ const iconButton = screen.getByRole("button")
+
+
+ expect(iconElement).toHaveAttribute('type', 'password');
+
+ fireEvent.click(iconButton);
+
+ expect(iconElement).toHaveAttribute("type", "text");
+
+ fireEvent.click(iconButton);
+
+ expect(iconElement).toHaveAttribute('type', 'password');
+ })
+ })
+})
\ No newline at end of file
diff --git a/src/core/components/PasswordField/index.jsx b/src/core/components/PasswordField/index.jsx
new file mode 100644
index 0000000..d109b73
--- /dev/null
+++ b/src/core/components/PasswordField/index.jsx
@@ -0,0 +1,33 @@
+import {useState} from "react";
+import {IconButton, InputAdornment, TextField} from "@mui/material";
+import {Visibility, VisibilityOff} from "@mui/icons-material";
+import {Field} from "formik";
+
+const PasswordField = (props) => {
+ const [showPassword, setShowPassword] = useState(false);
+
+ const handleClickShowPassword = () => {
+ setShowPassword(!showPassword);
+ };
+
+ return (
+
+
+ {showPassword ? : }
+
+
+ ),
+ }}
+ />
+ );
+};
+
+export default PasswordField;
diff --git a/src/core/components/PriceField.jsx b/src/core/components/PriceField.jsx
new file mode 100644
index 0000000..29d8763
--- /dev/null
+++ b/src/core/components/PriceField.jsx
@@ -0,0 +1,23 @@
+import {InputAdornment, TextField, Typography} from "@mui/material";
+import {useTranslations} from "next-intl";
+
+const PriceField = (props) => {
+ const t = useTranslations();
+ return (
+
+ {((props.value) / 10).toLocaleString()}
+ {t("ConfirmDialog.toman")}
+
+ ),
+ }}
+ {...props}
+ />
+ )
+}
+export default PriceField
\ No newline at end of file
diff --git a/src/core/components/StyledFab.jsx b/src/core/components/StyledFab.jsx
new file mode 100644
index 0000000..95c5ff7
--- /dev/null
+++ b/src/core/components/StyledFab.jsx
@@ -0,0 +1,10 @@
+import {Fab, styled} from "@mui/material";
+
+const StyledFab = styled(Fab)`
+ position: absolute;
+ bottom: 16px;
+ /* @noflip */
+ left: 16px;
+`;
+
+export default StyledFab;
diff --git a/src/core/components/StyledForm.jsx b/src/core/components/StyledForm.jsx
new file mode 100644
index 0000000..253fc12
--- /dev/null
+++ b/src/core/components/StyledForm.jsx
@@ -0,0 +1,6 @@
+import {Form} from "formik";
+import {styled} from "@mui/material/styles";
+
+const StyledForm = styled(Form)``;
+
+export default StyledForm;
diff --git a/src/core/components/StyledImage.jsx b/src/core/components/StyledImage.jsx
new file mode 100644
index 0000000..58dd65f
--- /dev/null
+++ b/src/core/components/StyledImage.jsx
@@ -0,0 +1,6 @@
+import {styled} from "@mui/material";
+import Image from "next/image";
+
+const StyledImage = styled(Image)``;
+
+export default StyledImage;
diff --git a/src/core/components/TitlePage.jsx b/src/core/components/TitlePage.jsx
new file mode 100644
index 0000000..77c72ee
--- /dev/null
+++ b/src/core/components/TitlePage.jsx
@@ -0,0 +1,15 @@
+import {useTranslations} from "next-intl";
+import Head from "next/head";
+
+const TitlePage = ({text}) => {
+ const t = useTranslations();
+ return (
+
+
+ {text ? `${t("app_short_name")} | ${t(text)}` : t("app_short_name")}
+
+
+ );
+};
+
+export default TitlePage;
diff --git a/src/core/components/UploadSystem.jsx b/src/core/components/UploadSystem.jsx
new file mode 100644
index 0000000..b40fb46
--- /dev/null
+++ b/src/core/components/UploadSystem.jsx
@@ -0,0 +1,175 @@
+import {Box, Button, Paper, Typography} from "@mui/material";
+import {useTranslations} from "next-intl";
+import AddIcon from "@mui/icons-material/Add";
+import DeleteForeverIcon from "@mui/icons-material/DeleteForever";
+import {useRef} from "react";
+
+const UploadSystem = ({
+ selectedImage,
+ setselectedImage,
+ handleUploadChange,
+ fieldname,
+ setFieldValue,
+ imageAlt,
+ imageSize,
+ fileType,
+ fileName,
+ setShowAddIcon,
+ showAddIcon,
+ }) => {
+ const t = useTranslations();
+ const fileInputRef = useRef(null);
+
+ const handleClick = () => {
+ fileInputRef.current.click();
+ };
+
+ const handleDeleteImage = () => {
+ setselectedImage(null);
+ setFieldValue(fieldname, null);
+ setShowAddIcon(true);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = "";
+ }
+ };
+
+ const isDocumentFormat = (fileType) => {
+ const documentFormats = [
+ "application/pdf",
+ "application/msword",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "application/vnd.ms-excel",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ ];
+ return documentFormats.includes(fileType);
+ };
+
+ return (
+
+ {showAddIcon ? (
+ // Show the add icon and "Upload File" text when no image is selected
+ <>
+
+
+
+ {t("UploadSystem.upload_file")}
+
+
+ >
+ ) : (
+ // Show the uploaded content along with the delete button when an image or document is selected
+ <>
+ {fileType && fileType.startsWith("image/") ? (
+
+
+
+ ) : (
+ fileType &&
+ isDocumentFormat(fileType) && (
+
+
+ {fileName}
+
+
+ )
+ )}
+
+ >
+ )}
+
+
+ );
+};
+
+export default UploadSystem;
diff --git a/src/core/components/notifications/ErrorNotification.jsx b/src/core/components/notifications/ErrorNotification.jsx
new file mode 100644
index 0000000..05e2fb9
--- /dev/null
+++ b/src/core/components/notifications/ErrorNotification.jsx
@@ -0,0 +1,39 @@
+import DangerousIcon from "@mui/icons-material/Dangerous";
+import {Box, Typography} from "@mui/material";
+import {toast} from "react-toastify";
+
+const ErrorNotification = (t, status, message) => {
+ toast(
+ () => (
+ <>
+
+
+
+
+
+ {t("notifications.error")} ({t("notifications.code")}: {status})
+
+
+ {message || t("notifications.error_static_text")}
+
+
+
+
+ >
+ ),
+ {
+ autoClose: false,
+ closeOnClick: false,
+ draggable: false,
+ }
+ );
+};
+
+export default ErrorNotification;
\ No newline at end of file
diff --git a/src/core/components/notifications/PendingNotification.jsx b/src/core/components/notifications/PendingNotification.jsx
new file mode 100644
index 0000000..6183f6b
--- /dev/null
+++ b/src/core/components/notifications/PendingNotification.jsx
@@ -0,0 +1,12 @@
+import {toast} from "react-toastify";
+
+const PendingNotification = (t) => {
+ toast(t("notifications.pending"), {
+ autoClose: false,
+ closeButton: false,
+ closeOnClick: false,
+ draggable: false,
+ });
+};
+
+export default PendingNotification;
\ No newline at end of file
diff --git a/src/core/components/notifications/SuccessNotification.jsx b/src/core/components/notifications/SuccessNotification.jsx
new file mode 100644
index 0000000..3507cef
--- /dev/null
+++ b/src/core/components/notifications/SuccessNotification.jsx
@@ -0,0 +1,42 @@
+import BeenhereIcon from "@mui/icons-material/Beenhere";
+import {Box, Typography} from "@mui/material";
+import {toast} from "react-toastify";
+
+const SuccessNotification = (t, status) => {
+ toast(
+ () => (
+ <>
+
+
+
+
+
+ {t("notifications.success")} ({t("notifications.code")}:{" "}
+ {status})
+
+
+ {t("notifications.success_static_text")}
+
+
+
+
+ >
+ ),
+ {
+ autoClose: 3000,
+ hideProgressBar: true,
+ pauseOnHover: true,
+ closeOnClick: false,
+ draggable: true,
+ }
+ );
+};
+
+export default SuccessNotification;
\ No newline at end of file
diff --git a/src/core/components/notifications/UploadFileNotification.jsx b/src/core/components/notifications/UploadFileNotification.jsx
new file mode 100644
index 0000000..89ebbb2
--- /dev/null
+++ b/src/core/components/notifications/UploadFileNotification.jsx
@@ -0,0 +1,38 @@
+import DangerousIcon from "@mui/icons-material/Dangerous";
+import {Box, Typography} from "@mui/material";
+import {toast} from "react-toastify";
+
+const UploadFileNotification = (t) => {
+ toast(
+ ({closeToast}) => (
+ <>
+
+
+
+
+
+ {t("UploadSystem.uploadfile_error")}
+
+
+
+
+ >
+ ),
+ {
+ autoClose: 3000,
+ hideProgressBar: true,
+ pauseOnHover: true,
+ closeOnClick: false,
+ draggable: true,
+ }
+ );
+};
+
+export default UploadFileNotification;
\ No newline at end of file
diff --git a/src/core/components/notifications/WarningNotification.jsx b/src/core/components/notifications/WarningNotification.jsx
new file mode 100644
index 0000000..19d6a2f
--- /dev/null
+++ b/src/core/components/notifications/WarningNotification.jsx
@@ -0,0 +1,40 @@
+import ReportIcon from "@mui/icons-material/Report";
+import {Box, Typography} from "@mui/material";
+import {toast} from "react-toastify";
+
+const WarningNotification = (t, status) => {
+ toast(
+ () => (
+ <>
+
+
+
+
+
+ {t("notifications.warning")} ({t("notifications.code")}:{" "}
+ {status})
+
+
+ {t("notifications.warning_static_text")}
+
+
+
+
+ >
+ ),
+ {
+ autoClose: false,
+ closeOnClick: false,
+ draggable: false,
+ }
+ );
+};
+
+export default WarningNotification;
\ No newline at end of file
diff --git a/src/core/components/notifications/index.jsx b/src/core/components/notifications/index.jsx
new file mode 100644
index 0000000..bc8eb66
--- /dev/null
+++ b/src/core/components/notifications/index.jsx
@@ -0,0 +1,54 @@
+import {toast} from "react-toastify";
+import ErrorNotification from "./ErrorNotification";
+import WarningNotification from "./WarningNotification";
+import SuccessNotification from "./SuccessNotification";
+
+const Notifications = async (t, response) => {
+ const {status, data} = response != undefined ? response : ""
+ toast.dismiss();
+ switch (status) {
+ case 200:
+ SuccessNotification(t, status);
+ break;
+ case 400:
+ ErrorNotification(t, status);
+ break;
+ case 401:
+ ErrorNotification(t, status);
+ break;
+ case 403:
+ ErrorNotification(t, status);
+ break;
+ case 422:
+ ErrorNotification(t, status, data.message);
+ break;
+ case 500:
+ WarningNotification(t, status);
+ break;
+ case 503:
+ WarningNotification(t, status);
+ break;
+ case 504:
+ WarningNotification(t, status);
+ break;
+ default:
+ toast(t("notifications.pending"), {
+ autoClose: false,
+ closeOnClick: false,
+ draggable: false,
+ });
+ break;
+ }
+};
+
+export default Notifications;
+
+/*
+usage document
+
+** for pending use ( Notifications( t, undefined) ) this before your request.
+** for success use ( Notifications( t, response) ) this inside .then() of your request.
+** for Error and Warning use ( Notifications( t, error.response) ) this inside .catche() of your request.
+
+end usage document
+*/
diff --git a/src/core/components/svgs/Svg403.jsx b/src/core/components/svgs/Svg403.jsx
new file mode 100644
index 0000000..d706ed0
--- /dev/null
+++ b/src/core/components/svgs/Svg403.jsx
@@ -0,0 +1,131 @@
+import {useTheme} from "@mui/material/styles";
+
+const Svg403 = ({width, height}) => {
+ const theme = useTheme()
+ const fillColor = theme.palette.primary.main
+
+ return (
+
+ )
+
+}
+
+export default Svg403
\ No newline at end of file
diff --git a/src/core/components/svgs/Svg404.jsx b/src/core/components/svgs/Svg404.jsx
new file mode 100644
index 0000000..159d7c6
--- /dev/null
+++ b/src/core/components/svgs/Svg404.jsx
@@ -0,0 +1,101 @@
+import {useTheme} from "@mui/material/styles";
+
+const Svg404 = ({width, height}) => {
+ const theme = useTheme()
+ const fillColor = theme.palette.primary.main
+
+ return (
+
+ )
+}
+
+export default Svg404
\ No newline at end of file
diff --git a/src/core/components/svgs/SvgChangePassword.jsx b/src/core/components/svgs/SvgChangePassword.jsx
new file mode 100644
index 0000000..1221d28
--- /dev/null
+++ b/src/core/components/svgs/SvgChangePassword.jsx
@@ -0,0 +1,77 @@
+import {useTheme} from "@mui/material/styles";
+
+const SvgChangePassword = ({width, height}) => {
+ const theme = useTheme()
+ const fillColor = theme.palette.primary.main
+
+ return (
+
+ )
+}
+
+export default SvgChangePassword
\ No newline at end of file
diff --git a/src/core/components/svgs/SvgDashboard.jsx b/src/core/components/svgs/SvgDashboard.jsx
new file mode 100644
index 0000000..a636bf4
--- /dev/null
+++ b/src/core/components/svgs/SvgDashboard.jsx
@@ -0,0 +1,117 @@
+import {useTheme} from "@mui/material/styles";
+
+const SvgDashboard = ({width, height}) => {
+ const theme = useTheme()
+ const fillColor = theme.palette.primary.main
+
+ return (
+
+ )
+}
+
+export default SvgDashboard
\ No newline at end of file
diff --git a/src/core/components/svgs/SvgLoading.jsx b/src/core/components/svgs/SvgLoading.jsx
new file mode 100644
index 0000000..9ff61ff
--- /dev/null
+++ b/src/core/components/svgs/SvgLoading.jsx
@@ -0,0 +1,136 @@
+import {useTheme} from "@mui/material/styles";
+
+const SvgLoading = ({width, height}) => {
+ const theme = useTheme()
+ const fillColor = theme.palette.primary.main
+
+ return (
+
+ )
+}
+
+export default SvgLoading
\ No newline at end of file
diff --git a/src/core/components/svgs/SvgLogin.jsx b/src/core/components/svgs/SvgLogin.jsx
new file mode 100644
index 0000000..edf3b51
--- /dev/null
+++ b/src/core/components/svgs/SvgLogin.jsx
@@ -0,0 +1,75 @@
+import {useTheme} from "@mui/material/styles";
+
+const SvgLogin = ({width, height}) => {
+ const theme = useTheme()
+ const fillColor = theme.palette.primary.main
+
+ return (
+
+ )
+
+}
+
+export default SvgLogin
\ No newline at end of file
diff --git a/src/core/data/apiRoutes.js b/src/core/data/apiRoutes.js
new file mode 100644
index 0000000..f949dac
--- /dev/null
+++ b/src/core/data/apiRoutes.js
@@ -0,0 +1,54 @@
+const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;
+
+//login
+export const GET_USER_TOKEN = BASE_URL + "/api/auth/login";
+//end login
+
+//edit profile
+export const UPDATE_AVATAR = BASE_URL + "";
+//end edit profile
+
+// role list
+export const GET_ROLE_LIST = BASE_URL + "/api/roles/list";
+// role list
+
+// province list
+export const GET_PROVINCE_LIST = BASE_URL + "/api/provinces";
+// province list
+
+// province list
+export const GET_CALLER_HISTORY = BASE_URL + "/api/calls/list";
+// province list
+
+//change password
+export const SET_USER_PASSWORD = BASE_URL + "/api/profile/change_password";
+//end change password
+
+//user data
+export const GET_USER = BASE_URL + "/api/profile/info";
+//user data
+
+//sidebar notification
+export const GET_SIDEBAR_NOTIFICATION = BASE_URL + "/dashboard/notification";
+//sidebar notification
+
+// role management
+export const GET_ROLES = BASE_URL + "/api/roles";
+export const ADD_ROLE = BASE_URL + "/api/roles";
+export const UPDATE_ROLE = BASE_URL + "/api/roles";
+export const DELETE_ROLE = BASE_URL + "/api/roles";
+
+export const GET_PERMISSIONS_LIST = BASE_URL + "/api/permissions/list";
+//role management
+
+//expert management
+export const GET_EXPERTS = BASE_URL + "/api/users";
+export const ADD_EXPERT = BASE_URL + "/api/users";
+export const UPDATE_EXPERT = BASE_URL + "/api/users";
+export const DELETE_EXPERT = BASE_URL + "/api/users";
+//expert management
+
+//call action history
+export const GET_CATEGORY = BASE_URL + "/api/categories";
+export const CALL_ACTION = BASE_URL + "/api/calls";
+//call action history
diff --git a/src/core/data/headerProfileItems.jsx b/src/core/data/headerProfileItems.jsx
new file mode 100644
index 0000000..577fba3
--- /dev/null
+++ b/src/core/data/headerProfileItems.jsx
@@ -0,0 +1,19 @@
+import BorderColorIcon from "@mui/icons-material/BorderColor";
+import PasswordIcon from "@mui/icons-material/Password";
+
+const headerProfileItems = [
+ {
+ key: 0,
+ name: "header.edit_profile",
+ route: "/dashboard/edit-profile",
+ icon: ,
+ },
+ {
+ key: 1,
+ name: "header.change_password",
+ route: "/dashboard/change-password",
+ icon: ,
+ },
+];
+
+export default headerProfileItems;
diff --git a/src/core/data/sidebarMenu.jsx b/src/core/data/sidebarMenu.jsx
new file mode 100644
index 0000000..5c14462
--- /dev/null
+++ b/src/core/data/sidebarMenu.jsx
@@ -0,0 +1,36 @@
+import SpaceDashboardIcon from "@mui/icons-material/SpaceDashboard";
+import AccessibilityIcon from '@mui/icons-material/Accessibility';
+import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
+
+const sidebarMenu = [
+ [
+ {
+ key: "sidebar.dashboard",
+ type: "page",
+ route: "/dashboard",
+ icon: ,
+ selected: false,
+ permission: "all",
+ },
+ {
+ key: "sidebar.expert-management",
+ type: "page",
+ name: "expert_management",
+ route: "/dashboard/expert-management",
+ icon: ,
+ selected: false,
+ permission: "all",
+ },
+ {
+ key: "sidebar.role-management",
+ name : "role_management",
+ type: "page",
+ route: "/dashboard/role-management",
+ icon: ,
+ selected: false,
+ permission: "manage_roles",
+ },
+ ],
+];
+
+export default sidebarMenu;
diff --git a/src/core/utils/createEmotionCache.jsx b/src/core/utils/createEmotionCache.jsx
new file mode 100644
index 0000000..413fafe
--- /dev/null
+++ b/src/core/utils/createEmotionCache.jsx
@@ -0,0 +1,38 @@
+import createCache from "@emotion/cache";
+import {prefixer} from "stylis";
+import stylisRTLPlugin from "stylis-plugin-rtl";
+
+const isBrowser = typeof document !== "undefined";
+
+export const createEmotionCacheLtr = () => {
+ let insertionPoint;
+
+ if (isBrowser) {
+ const emotionInsertionPoint = document.querySelector(
+ 'meta[name="emotion-insertion-point"]'
+ );
+ insertionPoint = emotionInsertionPoint ?? undefined;
+ }
+
+ return createCache({
+ key: "mui-style",
+ insertionPoint,
+ });
+};
+
+export const createEmotionCacheRtl = () => {
+ let insertionPoint;
+
+ if (isBrowser) {
+ const emotionInsertionPoint = document.querySelector(
+ 'meta[name="emotion-insertion-point"]'
+ );
+ insertionPoint = emotionInsertionPoint ?? undefined;
+ }
+
+ return createCache({
+ key: "muirtl",
+ stylisPlugins: [prefixer, stylisRTLPlugin],
+ insertionPoint,
+ });
+};
diff --git a/src/core/utils/errorHandler.js b/src/core/utils/errorHandler.js
new file mode 100644
index 0000000..8dbd364
--- /dev/null
+++ b/src/core/utils/errorHandler.js
@@ -0,0 +1,62 @@
+import ErrorNotification from "@/core/components/notifications/ErrorNotification";
+import WarningNotification from "@/core/components/notifications/WarningNotification";
+import {toast} from "react-toastify";
+
+export const errorSetting = (t, notification) => {
+ if (notification) toast.dismiss();
+}
+
+export const errorRequest = (t, notification) => {
+ if (notification) toast.dismiss();
+}
+
+export const errorResponse = (response, clearToken, t, notification) => {
+ if (notification) toast.dismiss();
+ if (isServerError(response.status)) {
+ errorServer(response, t, notification)
+ } else if (isClientError(response.status)) {
+ errorClient(response, clearToken, t, notification)
+ }
+}
+
+const errorServer = (response, t, notification) => {
+ if (notification) WarningNotification(t, response.status);
+}
+const errorClient = (response, clearToken, t, notification) => {
+ switch (response.status) {
+ case 401:
+ clearToken()
+ if (notification) ErrorNotification(t, response.status)
+ break;
+ case 422:
+ if ('type' in response.data) {
+ errorLogic(response, t, notification)
+ break;
+ }
+ errorValidation(response, t, notification)
+ break;
+ case 429:
+ if (notification) ErrorNotification(t, response.status)
+ break
+ default:
+ if (notification) ErrorNotification(t, response.status)
+ break
+ }
+}
+
+const isServerError = status => status >= 500 && status <= 599;
+const isClientError = status => status >= 400 && status <= 499;
+
+const errorLogic = (response, t, notification) => {
+ if (notification) ErrorNotification(t, response.status, response.data.message)
+}
+const errorValidation = (response, t, notification) => {
+ if (notification) {
+ const errorsMap = Object.keys(response.data.errors)
+ const errorsArray = response.data.errors
+
+ errorsMap.map((item, index) => {
+ ErrorNotification(t, response.status, errorsArray[item][0]);
+ })
+ }
+}
\ No newline at end of file
diff --git a/src/core/utils/succesHandler.js b/src/core/utils/succesHandler.js
new file mode 100644
index 0000000..a61ac85
--- /dev/null
+++ b/src/core/utils/succesHandler.js
@@ -0,0 +1,9 @@
+import SuccessNotification from "@/core/components/notifications/SuccessNotification";
+import {toast} from "react-toastify";
+
+export const successRequest = (response, t, options) => {
+ if (options.notification && options.success.notification.show) {
+ toast.dismiss();
+ SuccessNotification(t, response.status)
+ }
+}
\ No newline at end of file
diff --git a/src/core/utils/theme-rtl.jsx b/src/core/utils/theme-rtl.jsx
new file mode 100644
index 0000000..33a9ae7
--- /dev/null
+++ b/src/core/utils/theme-rtl.jsx
@@ -0,0 +1,14 @@
+import {createTheme} from "@mui/material/styles";
+import theme from "./theme";
+import {faIR} from "@mui/x-date-pickers/locales";
+
+const themeRtl = createTheme({
+ direction: "rtl",
+ typography: {
+ fontFamily: `IRANSansFaNum, sans-serif`,
+ },
+ faIR,
+ ...theme,
+});
+
+export default themeRtl;
diff --git a/src/core/utils/theme.jsx b/src/core/utils/theme.jsx
new file mode 100644
index 0000000..672e8a8
--- /dev/null
+++ b/src/core/utils/theme.jsx
@@ -0,0 +1,29 @@
+import {colord} from "colord";
+
+const theme = {
+ palette: {
+ primary: {
+ main: process.env.NEXT_PUBLIC_PRIMARY_MAIN,
+ contrastText: "#FFFFFF",
+ light: colord(process.env.NEXT_PUBLIC_PRIMARY_MAIN).lighten(0.1).toHex(),
+ dark: colord(process.env.NEXT_PUBLIC_PRIMARY_MAIN).darken(0.1).toHex(),
+ },
+ secondary: {
+ main: process.env.NEXT_PUBLIC_SECONDARY_MAIN,
+ contrastText: "#FFFFFF",
+ light: colord(process.env.NEXT_PUBLIC_SECONDARY_MAIN).lighten(0.1).toHex(),
+ dark: colord(process.env.NEXT_PUBLIC_SECONDARY_MAIN).darken(0.1).toHex(),
+ },
+ },
+ components: {
+ MuiBackdrop: {
+ styleOverrides: {
+ root: {
+ backgroundColor: colord(process.env.NEXT_PUBLIC_PRIMARY_MAIN).darken(0.2).alpha(0.7).toHex(),
+ },
+ },
+ },
+ },
+};
+
+export default theme;
diff --git a/src/layouts/AppLayout.jsx b/src/layouts/AppLayout.jsx
new file mode 100644
index 0000000..4fc5e1d
--- /dev/null
+++ b/src/layouts/AppLayout.jsx
@@ -0,0 +1,56 @@
+import theme from "@/core/utils/theme";
+import useLanguage from "@/lib/app/hooks/useLanguage";
+import useLoading from "@/lib/app/hooks/useLoading";
+import useUser from "@/lib/app/hooks/useUser";
+import NextNProgress from "nextjs-progressbar";
+import {useEffect} from "react";
+import {ToastContainer} from "react-toastify";
+import useDirection from "@/lib/app/hooks/useDirection";
+import "react-toastify/dist/ReactToastify.css";
+import NetworkComponent from "@/core/components/NetworkComponent";
+import GlobalHead from "@/core/components/GlobalHead";
+
+function AppLayout({children, isBot}) {
+ const {languageIsReady} = useLanguage();
+ const {setLoadingPage} = useLoading();
+ const {userChangedLanguage, token, isAuth} = useUser();
+ const {directionApp} = useDirection();
+
+ useEffect(() => {
+ if (languageIsReady) {
+ if (token) {
+ if (isAuth) {
+ if (userChangedLanguage) {
+ setLoadingPage(true);
+ return;
+ }
+ setLoadingPage(false);
+ return;
+ }
+ setLoadingPage(true);
+ return;
+ }
+ setLoadingPage(false);
+ return;
+ }
+ setLoadingPage(true);
+ }, [languageIsReady, token, isAuth, userChangedLanguage]);
+
+ if (!isBot) {
+ if (userChangedLanguage) return;
+ if (!languageIsReady) return;
+ }
+
+ return (<>
+
+
+
+
+ {children}
+ >);
+}
+
+export default AppLayout;
diff --git a/src/layouts/CenterLayout.jsx b/src/layouts/CenterLayout.jsx
new file mode 100644
index 0000000..1e1b8bf
--- /dev/null
+++ b/src/layouts/CenterLayout.jsx
@@ -0,0 +1,18 @@
+import {Fade, Stack} from "@mui/material";
+
+const CenterLayout = (props) => {
+ return (
+
+
+ {props.children}
+
+
+ );
+};
+
+export default CenterLayout;
diff --git a/src/layouts/DashboardLayout.jsx b/src/layouts/DashboardLayout.jsx
new file mode 100644
index 0000000..72344d6
--- /dev/null
+++ b/src/layouts/DashboardLayout.jsx
@@ -0,0 +1,17 @@
+import Dashboard from "src/components/layouts/Dashboard";
+import AppPermission from "@/middlewares/AppPermission";
+import CallWidget from "@/components/layouts/Dashboard/CallWidget";
+
+const DashboardLayout = (props) => {
+ const permissions = ["NEXT_PUBLIC_HAS_WIDGET"];
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export default DashboardLayout;
diff --git a/src/layouts/FullPageLayout.jsx b/src/layouts/FullPageLayout.jsx
new file mode 100644
index 0000000..d6ed61e
--- /dev/null
+++ b/src/layouts/FullPageLayout.jsx
@@ -0,0 +1,21 @@
+import {Stack} from "@mui/material";
+
+const FullPageLayout = (props) => {
+ return (
+
+ {props.children}
+
+ );
+};
+
+export default FullPageLayout;
diff --git a/src/layouts/MuiLayout.jsx b/src/layouts/MuiLayout.jsx
new file mode 100644
index 0000000..eba3449
--- /dev/null
+++ b/src/layouts/MuiLayout.jsx
@@ -0,0 +1,70 @@
+import NoSsrHandler from "@/core/components/NoSsrHandler";
+import {createEmotionCacheRtl} from "@/core/utils/createEmotionCache";
+import themeRtl from "@/core/utils/theme-rtl";
+import {CacheProvider} from "@emotion/react";
+import {GlobalStyles} from "@mui/material";
+import CssBaseline from "@mui/material/CssBaseline";
+import {ThemeProvider} from "@mui/material/styles";
+import Head from "next/head";
+
+const clientSideEmotionCacheRtl = createEmotionCacheRtl();
+
+const MuiLayout = ({children, isBot}) => {
+ const emotionCache = clientSideEmotionCacheRtl;
+ const theme = themeRtl;
+
+ return (
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+ );
+};
+
+export default MuiLayout;
diff --git a/src/layouts/ScrollableLayout.jsx b/src/layouts/ScrollableLayout.jsx
new file mode 100644
index 0000000..583edd7
--- /dev/null
+++ b/src/layouts/ScrollableLayout.jsx
@@ -0,0 +1,25 @@
+import {Box, Fade} from "@mui/material";
+
+const ScrollableLayout = (props) => {
+ const overflowY = props?.y || "hidden";
+ const overflowX = props?.x || "hidden";
+
+ return (
+
+
+ {props.children}
+
+
+ );
+};
+
+export default ScrollableLayout;
diff --git a/src/lib/app/contexts/language.jsx b/src/lib/app/contexts/language.jsx
new file mode 100644
index 0000000..e9e24f7
--- /dev/null
+++ b/src/lib/app/contexts/language.jsx
@@ -0,0 +1,97 @@
+import {FA_DATATABLE_LOCALIZATION} from "&/locales/fa/datatable";
+import {useRouter} from "next/router";
+import {createContext, useEffect, useState} from "react";
+import useUser from "../hooks/useUser";
+
+export const LanguageContext = createContext();
+
+export const LanguageProvider = ({children}) => {
+ const router = useRouter();
+ const languageList = [
+ {
+ key: "fa",
+ dir: "rtl",
+ name: "فارسی",
+ fontFamily: `IRANSans, sans-serif`,
+ tableLocalization: FA_DATATABLE_LOCALIZATION,
+ }
+ ];
+ const {user, userChangedLanguage, changeLanguageState} = useUser();
+ const [languageIsReady, setLanguageIsReady] = useState(false);
+ const [languageApp, setLanguageApp] = useState(process.env.NEXT_PUBLIC_DEFAULT_LANGUAGE);
+ const [directionApp, setDirectionApp] = useState(
+ process.env.NEXT_PUBLIC_DEFAULT_DIRECTION
+ );
+
+ useEffect(() => {
+ const lang = localStorage.getItem("_language");
+
+ if (lang) {
+ setLanguageApp(lang);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (!languageApp) return;
+
+ const lang = localStorage.getItem("_language");
+
+ if (!lang) {
+ localStorage.setItem("_language", languageApp);
+ } else {
+ if (languageApp != lang) {
+ localStorage.setItem("_language", languageApp);
+ }
+ }
+ }, [languageApp]);
+
+ useEffect(() => {
+ if (!router.isReady) return;
+ if (user.user_language) {
+ if (user.user_language != languageApp) {
+ setLanguageApp(user.user_language);
+ return;
+ }
+ }
+
+ if (languageApp != router.locale) {
+ router.replace(
+ {pathname: router.pathname, query: router.query},
+ router.asPath,
+ {
+ locale: languageApp,
+ }
+ );
+ return;
+ }
+ for (const lang of languageList) {
+ if (languageApp != lang.key) continue;
+ setDirectionApp(lang.dir);
+ document.dir = lang.dir;
+ }
+
+ const timer = setTimeout(() => {
+ changeLanguageState(false);
+ setLanguageIsReady(true);
+ }, 1000);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }, [router.locale, router.isReady, userChangedLanguage, languageApp]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/lib/app/contexts/loading.jsx b/src/lib/app/contexts/loading.jsx
new file mode 100644
index 0000000..f5b8a47
--- /dev/null
+++ b/src/lib/app/contexts/loading.jsx
@@ -0,0 +1,14 @@
+import LoadingHardPage from "@/core/components/LoadingHardPage";
+import {createContext, useState} from "react";
+
+export const LoadingContext = createContext();
+
+export const LoadingProvider = ({children}) => {
+ const [loadingPage, setLoadingPage] = useState(false);
+ return (
+
+
+ {children}
+
+ );
+};
diff --git a/src/lib/app/contexts/socket.jsx b/src/lib/app/contexts/socket.jsx
new file mode 100644
index 0000000..7d51194
--- /dev/null
+++ b/src/lib/app/contexts/socket.jsx
@@ -0,0 +1,68 @@
+import {createContext, useEffect, useMemo, useRef, useState} from "react";
+import {io} from "socket.io-client";
+import useUser from "@/lib/app/hooks/useUser";
+import {useTranslations} from "next-intl";
+import {toast} from "react-toastify";
+import PowerIcon from "@mui/icons-material/Power";
+import PowerOffIcon from "@mui/icons-material/PowerOff";
+
+export const SocketContext = createContext()
+
+export const SocketProvider = ({children}) => {
+ const {user, isAuth} = useUser()
+ const [connectionError, setConnectionError] = useState(false)
+ const socketToastId = useRef(null);
+ const t = useTranslations()
+ const socket = useMemo(() => io(process.env.NEXT_PUBLIC_SERVER_SOCKET_URL, {
+ autoConnect: false,
+ transports: ['websocket'],
+ auth: {
+ token: ""
+ }
+ }), [])
+
+ useEffect(() => {
+ if (isAuth) {
+ socket.auth.token = user.telephone_id
+ return
+ }
+ socket.auth.token = ''
+ }, [isAuth]);
+
+ useEffect(() => {
+ const connectErrorHandler = () => {
+ setConnectionError(true)
+ }
+ const connectHandler = () => {
+ setConnectionError(false)
+ }
+ socket.on("connect_error", connectErrorHandler);
+ socket.on("connect", connectHandler);
+
+ return () => {
+ socket.off("connect_error", connectErrorHandler);
+ socket.off("connect", connectHandler);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (!connectionError) {
+ toast.update(socketToastId.current, {
+ type: toast.TYPE.SUCCESS,
+ render: t('socket_is_connect_message'),
+ autoClose: 2000,
+ closeButton: true,
+ closeOnClick: true,
+ icon:
+ });
+ return
+ }
+ socketToastId.current = toast.warn(t('socket_is_not_connect_message'), {
+ draggable: false,
+ autoClose: false, closeButton: false, closeOnClick: false, icon:
+ })
+
+ }, [connectionError]);
+
+ return {children}
+}
\ No newline at end of file
diff --git a/src/lib/app/contexts/user.jsx b/src/lib/app/contexts/user.jsx
new file mode 100644
index 0000000..8bf29cc
--- /dev/null
+++ b/src/lib/app/contexts/user.jsx
@@ -0,0 +1,132 @@
+import {GET_USER} from "@/core/data/apiRoutes";
+import axios from "axios";
+import {createContext, useCallback, useEffect, useReducer} from "react";
+import {errorRequest, errorResponse, errorSetting} from "@/core/utils/errorHandler";
+
+const initialUser = {
+ isAuth: false,
+ userChangedLanguage: false,
+ token: null,
+ user: {},
+};
+
+const reducer = (state, action) => {
+ switch (action.type) {
+ case "CLEAR_USER":
+ return {...state, user: {}};
+ case "CHANGE_USER":
+ return {...state, user: action.user};
+ case "CHANGE_USER_LANGUAGE":
+ return {
+ ...state,
+ user: {...state.user, user_language: action.language},
+ };
+ case "CHANGE_AUTH_STATE":
+ return {...state, isAuth: action.isAuth};
+ case "CHANGE_LANGUAGE_STATE":
+ return {...state, userChangedLanguage: action.userChangedLanguage};
+ case "CLEAR_TOKEN":
+ localStorage.removeItem("_token");
+ return {...state, token: null};
+ case "SET_TOKEN":
+ localStorage.setItem("_token", action.token);
+ return {...state, token: action.token};
+ default:
+ return state;
+ }
+};
+
+export const UserContext = createContext();
+
+export const UserProvider = ({children}) => {
+ const [state, dispatch] = useReducer(reducer, initialUser);
+
+ const clearUser = useCallback(() => {
+ dispatch({type: "CLEAR_USER"});
+ }, []);
+
+ const changeUser = useCallback((user) => {
+ dispatch({type: "CHANGE_USER", user});
+ }, []);
+
+ const changeUserLanguage = useCallback(/* use in multi language app */);
+
+ const changeAuthState = useCallback((isAuth) => {
+ dispatch({type: "CHANGE_AUTH_STATE", isAuth});
+ }, []);
+
+ const changeLanguageState = useCallback((userChangedLanguage) => {
+ dispatch({type: "CHANGE_LANGUAGE_STATE", userChangedLanguage});
+ }, []);
+
+ const clearToken = useCallback(() => {
+ dispatch({type: "CLEAR_TOKEN"});
+ }, []);
+
+ const setToken = useCallback((token) => {
+ dispatch({type: "SET_TOKEN", token});
+ }, []);
+
+ const getUser = useCallback(
+ (callback = () => {
+ }) => {
+ axios
+ .get(GET_USER, {
+ headers: {authorization: `Bearer ${state.token}`},
+ })
+ .then(({data}) => {
+ if (typeof callback === "function") callback(data.data);
+ })
+ .catch(error => {
+ if (error.response) {
+ errorResponse(error.response, clearToken, null, false)
+ } else if (error.request) {
+ errorRequest(null, false)
+ } else {
+ errorSetting(null, false)
+ }
+ })
+ },
+ [state.token]
+ );
+
+ useEffect(() => {
+ const localToken = localStorage.getItem("_token");
+ if (localToken) dispatch({type: "SET_TOKEN", token: localToken});
+ }, []);
+
+ useEffect(() => {
+ if (!state.token) {
+ clearUser();
+ changeAuthState(false);
+ changeLanguageState(false);
+ return;
+ }
+ getUser((data) => {
+ changeUser(data);
+ changeAuthState(true);
+ changeLanguageState(true);
+ });
+ }, [state.token]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/lib/app/hooks/useDirection.jsx b/src/lib/app/hooks/useDirection.jsx
new file mode 100644
index 0000000..b2e01fe
--- /dev/null
+++ b/src/lib/app/hooks/useDirection.jsx
@@ -0,0 +1,10 @@
+import {useContext} from "react";
+import {LanguageContext} from "../contexts/language";
+
+const useDirection = () => {
+ const {directionApp} = useContext(LanguageContext);
+
+ return {directionApp};
+};
+
+export default useDirection;
diff --git a/src/lib/app/hooks/useLanguage.jsx b/src/lib/app/hooks/useLanguage.jsx
new file mode 100644
index 0000000..d663835
--- /dev/null
+++ b/src/lib/app/hooks/useLanguage.jsx
@@ -0,0 +1,28 @@
+import {useContext} from "react";
+import {LanguageContext} from "../contexts/language";
+import useUser from "./useUser";
+
+const useLanguage = () => {
+ const {isAuth, changeUserLanguage} = useUser();
+ const {
+ languageApp,
+ setLanguageApp,
+ languageIsReady,
+ setLanguageIsReady,
+ languageList,
+ } = useContext(LanguageContext);
+
+ const changeLanguage = (lang) => {
+ if (lang == languageApp) return;
+
+ setLanguageIsReady(false);
+ setLanguageApp(lang);
+ if (isAuth) {
+ changeUserLanguage(lang);
+ }
+ };
+
+ return {languageApp, changeLanguage, languageIsReady, languageList};
+};
+
+export default useLanguage;
diff --git a/src/lib/app/hooks/useLoading.jsx b/src/lib/app/hooks/useLoading.jsx
new file mode 100644
index 0000000..a691bd7
--- /dev/null
+++ b/src/lib/app/hooks/useLoading.jsx
@@ -0,0 +1,10 @@
+import {useContext} from "react";
+import {LoadingContext} from "../contexts/loading";
+
+const useLoading = () => {
+ const {setLoadingPage} = useContext(LoadingContext);
+
+ return {setLoadingPage};
+};
+
+export default useLoading;
diff --git a/src/lib/app/hooks/useNetwork.jsx b/src/lib/app/hooks/useNetwork.jsx
new file mode 100644
index 0000000..9c436a5
--- /dev/null
+++ b/src/lib/app/hooks/useNetwork.jsx
@@ -0,0 +1,34 @@
+import {useEffect, useState} from "react";
+
+function useNetwork() {
+ const [state, setState] = useState(() => {
+ return {
+ online: navigator.onLine,
+ };
+ });
+ useEffect(() => {
+ const handleOnline = () => {
+ setState((prevState) => ({
+ ...prevState,
+ online: true,
+ }));
+ };
+ const handleOffline = () => {
+ setState((prevState) => ({
+ ...prevState,
+ online: false,
+ }));
+ };
+
+ window.addEventListener("online", handleOnline);
+ window.addEventListener("offline", handleOffline);
+ return () => {
+ window.removeEventListener("online", handleOnline);
+ window.removeEventListener("offline", handleOffline);
+ };
+ }, []);
+
+ return state;
+}
+
+export default useNetwork;
\ No newline at end of file
diff --git a/src/lib/app/hooks/useNotification.jsx b/src/lib/app/hooks/useNotification.jsx
new file mode 100644
index 0000000..6e80bca
--- /dev/null
+++ b/src/lib/app/hooks/useNotification.jsx
@@ -0,0 +1,26 @@
+import useSWR from 'swr'
+import {GET_SIDEBAR_NOTIFICATION} from "@/core/data/apiRoutes";
+import useRequest from "@/lib/app/hooks/useRequest";
+const GLOBAL_HAS_VALUE = process.env.NEXT_PUBLIC_HAS_VALUE;
+const hasPermissionsValue = GLOBAL_HAS_VALUE ? JSON.parse(GLOBAL_HAS_VALUE) : [];
+const isNotificationEnabled = hasPermissionsValue.includes("NEXT_PUBLIC_HAS_NOTIFICATION");
+
+const useNotification = () => {
+
+ const requestServer = useRequest({auth: true, notification: false})
+
+ //swr config
+ const fetcher = (...args) => {
+ return requestServer(args, 'get').then((response) => {
+ return response.data.data;
+ }).catch(() => {
+ })
+ };
+
+ const {data, mutate} = useSWR( isNotificationEnabled ? GET_SIDEBAR_NOTIFICATION : '', fetcher , {keepPreviousData:true})
+ //swr config
+
+ // render data
+ return {notification_count: data, update_notification: mutate}
+}
+export default useNotification
\ No newline at end of file
diff --git a/src/lib/app/hooks/usePermissions.jsx b/src/lib/app/hooks/usePermissions.jsx
new file mode 100644
index 0000000..2e6fff5
--- /dev/null
+++ b/src/lib/app/hooks/usePermissions.jsx
@@ -0,0 +1,26 @@
+import useRequest from "@/lib/app/hooks/useRequest";
+import useSWR from "swr";
+import {GET_PERMISSIONS_LIST} from "@/core/data/apiRoutes";
+
+const usePermissions = () => {
+ const requestServer = useRequest({auth: true, notification: false})
+ const fetcher = (...args) => {
+ return requestServer(args, 'get').then((response) => {
+ return response.data.data;
+ }).catch(() => {
+ })
+ };
+
+ const {data, isLoading} = useSWR(GET_PERMISSIONS_LIST, fetcher, {
+ revalidateIfStale: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ keepPreviousData : true
+ })
+ const permissions_list = data
+ //swr config
+
+ // render data
+ return {permissions_list, isLoading}
+}
+export default usePermissions;
\ No newline at end of file
diff --git a/src/lib/app/hooks/useProvince.jsx b/src/lib/app/hooks/useProvince.jsx
new file mode 100644
index 0000000..2bc8c6b
--- /dev/null
+++ b/src/lib/app/hooks/useProvince.jsx
@@ -0,0 +1,30 @@
+import {GET_PROVINCE_LIST} from "@/core/data/apiRoutes";
+import useRequest from "@/lib/app/hooks/useRequest";
+import useSWR from "swr";
+
+const useProvince = () => {
+ const requestServer = useRequest({auth: true, notification: false})
+
+ //swr config
+ const fetcher = (...args) => {
+ return requestServer(args, 'get').then(({data}) => {
+ return data.data;
+ }).catch(() => {
+ })
+ };
+
+ const {data, isLoading} = useSWR(GET_PROVINCE_LIST, fetcher, {
+ revalidateIfStale: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ keepPreviousData: true
+ });
+
+ return {
+ provinceList: data,
+ isLoadingProvinceList: isLoading,
+ errorProvinceList: !data
+ }
+};
+
+export default useProvince;
\ No newline at end of file
diff --git a/src/lib/app/hooks/useRequest.jsx b/src/lib/app/hooks/useRequest.jsx
new file mode 100644
index 0000000..d361e08
--- /dev/null
+++ b/src/lib/app/hooks/useRequest.jsx
@@ -0,0 +1,66 @@
+import axios from "axios";
+import {successRequest} from "@/core/utils/succesHandler";
+import PendingNotification from "@/core/components/notifications/PendingNotification";
+import {useTranslations} from "next-intl";
+import useUser from "@/lib/app/hooks/useUser";
+import {errorRequest, errorResponse, errorSetting} from "@/core/utils/errorHandler";
+import useNetwork from "@/lib/app/hooks/useNetwork";
+
+const defaultOptions = {
+ auth: false, data: {}, requestOptions: {
+ headers: {}
+ }, notification: true, pending: true, success: {
+ notification: {
+ show: true,
+ },
+ }, failed: {
+ notification: {
+ show: true,
+ },
+ },
+}
+const useRequest = (initOptions) => {
+ const network = useNetwork()
+ const t = useTranslations()
+ const {token, clearToken} = useUser()
+ let _options = {...defaultOptions, ...initOptions}
+
+ function requestServer(url = '', method = 'get', options) {
+ _options = {..._options, ...options}
+ if (_options.auth) _options = {
+ ..._options, requestOptions: {
+ ..._options.requestOptions,
+ headers: {..._options.requestOptions.headers, authorization: `Bearer ${token}`}
+ }
+ }
+
+ return new Promise((resolve, reject) => {
+ if (!network.online) {
+ reject()
+ return
+ }
+ if (_options.notification && _options.failed.notification.show && _options.pending) PendingNotification(t)
+ axios({
+ url: url, method: method, data: _options.data, ..._options.requestOptions
+ })
+ .then(response => {
+ successRequest(response, t, _options)
+ resolve(response)
+ })
+ .catch(error => {
+ if (error.response) {
+ errorResponse(error.response, clearToken, t, _options.notification && _options.failed.notification.show)
+ } else if (error.request) {
+ errorRequest(t, _options.notification && _options.failed.notification.show)
+ } else {
+ errorSetting(t, _options.notification && _options.failed.notification.show)
+ }
+ reject(error)
+ })
+ });
+ }
+
+ return requestServer
+}
+
+export default useRequest
\ No newline at end of file
diff --git a/src/lib/app/hooks/useRole.jsx b/src/lib/app/hooks/useRole.jsx
new file mode 100644
index 0000000..b4c231c
--- /dev/null
+++ b/src/lib/app/hooks/useRole.jsx
@@ -0,0 +1,30 @@
+import {GET_ROLE_LIST} from "@/core/data/apiRoutes";
+import useRequest from "@/lib/app/hooks/useRequest";
+import useSWR from "swr";
+
+const useRole = () => {
+ const requestServer = useRequest({auth: true, notification: false})
+
+ //swr config
+ const fetcher = (...args) => {
+ return requestServer(args, 'get').then(({data}) => {
+ return data.data;
+ }).catch(() => {
+ })
+ };
+
+ const {data, isLoading} = useSWR(GET_ROLE_LIST, fetcher, {
+ revalidateIfStale: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ keepPreviousData: true
+ });
+
+ return {
+ roleList: data,
+ isLoadingRoleList: isLoading,
+ errorRoleList: !data
+ }
+};
+
+export default useRole;
\ No newline at end of file
diff --git a/src/lib/app/hooks/useSocket.jsx b/src/lib/app/hooks/useSocket.jsx
new file mode 100644
index 0000000..3b9855d
--- /dev/null
+++ b/src/lib/app/hooks/useSocket.jsx
@@ -0,0 +1,10 @@
+import {useContext} from "react";
+import {SocketContext} from "@/lib/app/contexts/socket";
+
+const useSocket = () => {
+ const {socket} = useContext(SocketContext)
+
+ return socket
+}
+
+export default useSocket
\ No newline at end of file
diff --git a/src/lib/app/hooks/useUser.jsx b/src/lib/app/hooks/useUser.jsx
new file mode 100644
index 0000000..b0bbc1a
--- /dev/null
+++ b/src/lib/app/hooks/useUser.jsx
@@ -0,0 +1,34 @@
+import {useContext} from "react";
+import {UserContext} from "../contexts/user";
+
+const useUser = () => {
+ const {
+ isAuth,
+ userChangedLanguage,
+ token,
+ user,
+ getUser,
+ clearUser,
+ changeUser,
+ changeAuthState,
+ changeLanguageState,
+ clearToken,
+ setToken,
+ } = useContext(UserContext);
+
+ return {
+ isAuth,
+ userChangedLanguage,
+ token,
+ user,
+ getUser,
+ clearUser,
+ changeUser,
+ changeAuthState,
+ changeLanguageState,
+ clearToken,
+ setToken,
+ };
+};
+
+export default useUser;
diff --git a/src/lib/callWidget/contexts/answers.jsx b/src/lib/callWidget/contexts/answers.jsx
new file mode 100644
index 0000000..96a150c
--- /dev/null
+++ b/src/lib/callWidget/contexts/answers.jsx
@@ -0,0 +1,86 @@
+import { createContext, useEffect, useReducer, useState } from "react";
+import { GET_CATEGORY } from "@/core/data/apiRoutes";
+import useRequest from "@/lib/app/hooks/useRequest";
+
+const reducer = (state, action) => {
+ switch (action.type) {
+ case "CHANGE_ACTIVE_TAB":
+ return state.map((answer, index) =>
+ action.newValue === index
+ ? { ...answer, active: true }
+ : {
+ ...answer,
+ active: false,
+ }
+ );
+ case "CHANGE_LIST":
+ return [...action.newList];
+ case "CHANGE_ACTIVE_ID":
+ return state.map((answer, index) =>
+ action.tab_id === answer.id
+ ? {
+ ...answer,
+ active_category_id: action.category_id,
+ }
+ : { ...answer }
+ );
+ default:
+ return state;
+ }
+};
+
+const submission = (handleCategories, operation) => {
+ switch (operation.type) {
+ case "SET_CATEGORIES":
+ return { ...handleCategories, categoryLists: operation.categoriesByAPI };
+ case "SET_SUBCATEGORIES":
+ return { ...handleCategories, subCategoryLists: operation.subCategoriesByAPI };
+ default:
+ return handleCategories;
+ }
+};
+
+export const AnswersContext = createContext();
+export const AnswersProvider = ({ children }) => {
+ const requestServer = useRequest({ auth: true, notification: false });
+ const [openCallDialog, setOpenCallDialog] = useState(false);
+ const [activeTab, setActiveTab] = useState(0);
+ const [handleCategories, operation] = useReducer(submission, {
+ categoryLists: [],
+ subCategoryLists: [],
+ });
+ const [state, dispatch] = useReducer(reducer, []);
+
+ useEffect(() => {
+ requestServer(GET_CATEGORY, "get")
+ .then(({ data }) => {
+ const subCategoriesByAPI = data.data;
+ const categoriesByAPI = subCategoriesByAPI.reduce((accumulator, currentValue) => {
+ if (!accumulator.find((item) => item.category_id === currentValue.category_id)) {
+ accumulator.push(currentValue);
+ }
+ return accumulator;
+ }, []);
+ operation({ type: "SET_CATEGORIES", categoriesByAPI });
+ operation({ type: "SET_SUBCATEGORIES", subCategoriesByAPI });
+ })
+ .catch(() => {});
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/lib/callWidget/contexts/categories.jsx b/src/lib/callWidget/contexts/categories.jsx
new file mode 100644
index 0000000..533dfc3
--- /dev/null
+++ b/src/lib/callWidget/contexts/categories.jsx
@@ -0,0 +1,59 @@
+import { createContext, useCallback, useEffect, useReducer } from "react";
+import useRequest from "@/lib/app/hooks/useRequest";
+import { GET_CATEGORY } from "@/core/data/apiRoutes";
+
+const reducer = (state, action) => {
+ switch (action.type) {
+ case "CHANGE_ACTIVE_ID":
+ return { ...state, active_id: action.id };
+ case "SET_CATEGORIES":
+ return { ...state, categoryLists: action.cat };
+ case "SET_SUBCATEGORIES":
+ return { ...state, subCategoryLists: action.subCat };
+ default:
+ return state;
+ }
+};
+export const CategoriesContext = createContext();
+
+export const CategoriesProvider = ({ children }) => {
+ const requestServer = useRequest({ auth: true, notification: false });
+ const [state, dispatch] = useReducer(reducer, {
+ categoryLists: [],
+ subCategoryLists: [],
+ active_id: null,
+ });
+
+ useEffect(() => {
+ requestServer(GET_CATEGORY, "get")
+ .then(({ data }) => {
+ const subCat = data.data;
+ const cat = subCat.reduce((accumulator, currentValue) => {
+ if (!accumulator.find((item) => item.category_id === currentValue.category_id)) {
+ accumulator.push(currentValue);
+ }
+ return accumulator;
+ }, []);
+ dispatch({ type: "SET_CATEGORIES", cat });
+ dispatch({ type: "SET_SUBCATEGORIES", subCat });
+ })
+ .catch(() => {});
+ }, []);
+
+ const setCategoryID = useCallback((id) => {
+ dispatch({ type: "CHANGE_ACTIVE_ID", id });
+ });
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/lib/callWidget/hooks/useAnswers.jsx b/src/lib/callWidget/hooks/useAnswers.jsx
new file mode 100644
index 0000000..149b1d3
--- /dev/null
+++ b/src/lib/callWidget/hooks/useAnswers.jsx
@@ -0,0 +1,46 @@
+import { useContext } from "react";
+import { AnswersContext } from "@/lib/callWidget/contexts/answers";
+
+const useAnswers = () => {
+ const { state, dispatch, activeTab, setActiveTab } = useContext(AnswersContext);
+
+ const changeActiveTabAnswers = (newValue) => {
+ dispatch({ type: "CHANGE_ACTIVE_TAB", newValue });
+ setActiveTab(newValue);
+ };
+ const newAnswerTab = (data) => {
+ const newAnswer = {
+ id: data.id,
+ phone_number: data.phone_number,
+ date: new Date(),
+ active: true,
+ active_category_id: null,
+ };
+ const newList = [...state, newAnswer];
+ dispatch({ type: "CHANGE_LIST", newList });
+ changeActiveTabAnswers(newList.length - 1);
+ };
+
+ const closeAnswerTab = (id) => {
+ const index = state.findIndex((answer) => answer.id === id);
+ const newIndex = index === 0 ? 0 : index - 1;
+ const newList = [...state];
+ newList.splice(index, 1);
+ if (newList.length !== 0) {
+ newList[newIndex].active = true;
+ setActiveTab(newIndex);
+ }
+ dispatch({ type: "CHANGE_LIST", newList });
+ };
+
+ return {
+ answersList: state,
+ activeTab,
+ setActiveTab,
+ changeActiveTabAnswers,
+ closeAnswerTab,
+ newAnswerTab,
+ };
+};
+
+export default useAnswers;
diff --git a/src/lib/callWidget/hooks/useCallDialog.jsx b/src/lib/callWidget/hooks/useCallDialog.jsx
new file mode 100644
index 0000000..30dd599
--- /dev/null
+++ b/src/lib/callWidget/hooks/useCallDialog.jsx
@@ -0,0 +1,10 @@
+import {useContext} from "react";
+import {AnswersContext} from "@/lib/callWidget/contexts/answers";
+
+const useCallDialog = () => {
+ const {openCallDialog, setOpenCallDialog} = useContext(AnswersContext)
+
+ return {openCallDialog, setOpenCallDialog}
+}
+
+export default useCallDialog
\ No newline at end of file
diff --git a/src/lib/callWidget/hooks/useCallerHistory.jsx b/src/lib/callWidget/hooks/useCallerHistory.jsx
new file mode 100644
index 0000000..810209d
--- /dev/null
+++ b/src/lib/callWidget/hooks/useCallerHistory.jsx
@@ -0,0 +1,37 @@
+import useRequest from "@/lib/app/hooks/useRequest";
+import {useEffect, useState} from "react";
+import useSWR from "swr";
+import {GET_CALLER_HISTORY} from "@/core/data/apiRoutes";
+
+const useCallerHistory = (call_id, phone_number, max_size) => {
+ const requestServer = useRequest({auth: true, notification: false});
+ const [validData, setValidData] = useState([]);
+
+ //swr config
+ const fetcher = (...args) => {
+ return requestServer(args, 'get').then(({data}) => {
+ return data.data;
+ }).catch(() => {
+ })
+ };
+
+ const {data, isLoading} = useSWR(`${GET_CALLER_HISTORY}?phone_number=${phone_number}&size=${max_size}`, fetcher, {
+ revalidateIfStale: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ keepPreviousData: true
+ });
+
+ useEffect(() => {
+ data && setValidData(data.filter((item) => item.id != call_id));
+ }, [data, call_id]);
+
+ return {
+ firstItemOfHistory: validData.length === 0 ? false : validData[0],
+ historyList: validData.length < 2 ? false : validData.slice(1),
+ isLoadingHistoryList: isLoading,
+ errorHistoryList: !data
+ }
+};
+
+export default useCallerHistory;
\ No newline at end of file
diff --git a/src/lib/callWidget/hooks/useCategories.jsx b/src/lib/callWidget/hooks/useCategories.jsx
new file mode 100644
index 0000000..423bd67
--- /dev/null
+++ b/src/lib/callWidget/hooks/useCategories.jsx
@@ -0,0 +1,17 @@
+import { useContext } from "react";
+import { AnswersContext } from "@/lib/callWidget/contexts/answers";
+
+const useCategories = () => {
+ const { categoryLists, subCategoryLists, active_category_id, dispatch } = useContext(AnswersContext);
+
+ const setActiveCategoryID = (tab_id, category_id) => {
+ dispatch({ type: "CHANGE_ACTIVE_ID", tab_id, category_id });
+ };
+ return {
+ categoryLists,
+ subCategoryLists,
+ active_category_id,
+ setActiveCategoryID,
+ };
+};
+export default useCategories;
diff --git a/src/middlewares/AppPermission.jsx b/src/middlewares/AppPermission.jsx
new file mode 100644
index 0000000..8f93bd3
--- /dev/null
+++ b/src/middlewares/AppPermission.jsx
@@ -0,0 +1,8 @@
+const GLOBAL_HAS_VALUE = process.env.NEXT_PUBLIC_HAS_VALUE;
+const hasPermissionsValue = GLOBAL_HAS_VALUE ? JSON.parse(GLOBAL_HAS_VALUE) : [];
+
+const AppPermission = ({ children , permissions }) => {
+ return permissions.some(permission => hasPermissionsValue.includes(permission)) ? <>{children}> : null;
+};
+
+export default AppPermission;
diff --git a/src/middlewares/RolePermission.jsx b/src/middlewares/RolePermission.jsx
new file mode 100644
index 0000000..4b1cc8a
--- /dev/null
+++ b/src/middlewares/RolePermission.jsx
@@ -0,0 +1,14 @@
+import useUser from "@/lib/app/hooks/useUser";
+import RolePermissionComponent from "@/core/components/Middleware/RolePermissionComponent";
+
+const RolePermissionMiddleware = ({children, requiredPermissions}) => {
+ const {user} = useUser();
+
+ const hasPermission = requiredPermissions.some((permission) =>
+ user?.permissions?.includes(permission)
+ );
+
+ return !hasPermission ? : <>{children}>;
+};
+
+export default RolePermissionMiddleware;
diff --git a/src/middlewares/WithAuth.jsx b/src/middlewares/WithAuth.jsx
new file mode 100644
index 0000000..d3b0493
--- /dev/null
+++ b/src/middlewares/WithAuth.jsx
@@ -0,0 +1,11 @@
+import useUser from "@/lib/app/hooks/useUser";
+import WithAuthComponent from "@/core/components/Middleware/WithAuthComponent";
+
+
+const WithAuthMiddleware = ({children}) => {
+ const {isAuth} = useUser();
+
+ return isAuth ? <>{children}> : ;
+};
+
+export default WithAuthMiddleware;
diff --git a/src/middlewares/WithoutAuth.jsx b/src/middlewares/WithoutAuth.jsx
new file mode 100644
index 0000000..7fbcba6
--- /dev/null
+++ b/src/middlewares/WithoutAuth.jsx
@@ -0,0 +1,34 @@
+import useUser from "@/lib/app/hooks/useUser";
+import {useRouter} from "next/router";
+import {useEffect} from "react";
+import WithoutAuthComponent from "@/core/components/Middleware/WithoutAuthComponent";
+
+const WithoutAuthMiddleware = ({children}) => {
+ const {isAuth} = useUser();
+ const router = useRouter();
+
+ const backUrlDecodedPath = router.query?.back_url;
+
+ useEffect(() => {
+ if (!isAuth) return;
+ const timer = setTimeout(() => {
+ router.replace(
+ backUrlDecodedPath
+ ? decodeURIComponent(backUrlDecodedPath)
+ : "/dashboard"
+ );
+ }, 2000);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }, [isAuth]);
+
+ return isAuth ? (
+
+ ) : (
+ <>{children}>
+ );
+};
+
+export default WithoutAuthMiddleware;
diff --git a/src/middlewares/__test__/AppPermission.test.js b/src/middlewares/__test__/AppPermission.test.js
new file mode 100644
index 0000000..82377ba
--- /dev/null
+++ b/src/middlewares/__test__/AppPermission.test.js
@@ -0,0 +1,26 @@
+import {render, screen, waitFor} from '@testing-library/react';
+import MockAppWithProviders from "../../../mocks/AppWithProvider";
+import AppPermission from "@/middlewares/AppPermission";
+
+describe('AppPermission From middlewares', () => {
+ describe('Behavior', () => {
+ it('show related child if permission exist', async () => {
+
+ const requiredPermissions = ["NEXT_PUBLIC_HAS_WIDGET"];
+ render({'children'});
+
+ await waitFor(()=>{
+ expect(screen.queryByText('children')).toBeInTheDocument();
+ })
+ });
+ it('do not show related child if permission does not exist', async () => {
+
+ const requiredPermissions = ["MISSING_PERMISSION"];
+ render();
+
+ await waitFor(() => {
+ expect(screen.queryByText('children')).not.toBeInTheDocument();
+ });
+ });
+ });
+});
diff --git a/src/middlewares/__test__/RolePermission.test.js b/src/middlewares/__test__/RolePermission.test.js
new file mode 100644
index 0000000..f37796f
--- /dev/null
+++ b/src/middlewares/__test__/RolePermission.test.js
@@ -0,0 +1,34 @@
+import {render, screen, waitFor} from '@testing-library/react';
+import MockAppWithProviders from "../../../mocks/AppWithProvider";
+import RolePermissionMiddleware from "@/middlewares/RolePermission";
+
+
+describe('RolePermissionMiddleware From RolePermissionMiddleware', () => {
+ describe('Behavior', () => {
+ it('show related child if permission exist', async () => {
+
+ const requiredPermissions = ["manage_users"];
+ render({'children'});
+
+ await waitFor(()=>{
+ expect(screen.queryByText('children')).toBeInTheDocument();
+
+ })
+
+ });
+ it('show related error if permission not exist', async () => {
+
+ const requiredPermissions = ["other-permission"];
+
+ render();
+
+ await waitFor(()=>{
+
+ expect(screen.queryByText('شما دسترسی لازم برای مشاهده این صفحه را ندارید!')).toBeInTheDocument();
+
+ })
+
+
+ });
+ });
+});
diff --git a/src/pages/403.jsx b/src/pages/403.jsx
new file mode 100644
index 0000000..15ba915
--- /dev/null
+++ b/src/pages/403.jsx
@@ -0,0 +1,24 @@
+import {useEffect, useState} from "react";
+import {useRouter} from "next/router";
+import {NextIntlProvider} from "next-intl";
+import UnAuthorizedComponent from "@/components/errors/403";
+
+export default function Custom404() {
+ const router = useRouter()
+ const [messages, setMessages] = useState(null)
+ useEffect(() => {
+ const fetch = async () => {
+ const tempMessages = (await import(`&/locales/${router.locale}/app.json`)).default
+ setMessages(tempMessages)
+ }
+ fetch()
+ }, []);
+
+ if (!messages) return
+
+ return (
+
+
+
+ );
+}
diff --git a/src/pages/404.jsx b/src/pages/404.jsx
new file mode 100644
index 0000000..26de038
--- /dev/null
+++ b/src/pages/404.jsx
@@ -0,0 +1,25 @@
+import {useEffect, useState} from "react";
+import {useRouter} from "next/router";
+import {NextIntlProvider} from "next-intl";
+import NotFoundComponent from "@/components/errors/404";
+
+export default function Custom404() {
+ const router = useRouter()
+ const [messages, setMessages] = useState(null)
+ useEffect(() => {
+
+ const fetch = async () => {
+ const tempMessages = (await import(`&/locales/${router.locale}/app.json`)).default
+ setMessages(tempMessages)
+ }
+ fetch()
+ }, []);
+
+ if (!messages) return
+
+ return (
+
+
+
+ );
+}
diff --git a/src/pages/500.jsx b/src/pages/500.jsx
new file mode 100644
index 0000000..4b85bc2
--- /dev/null
+++ b/src/pages/500.jsx
@@ -0,0 +1,24 @@
+import {useEffect, useState} from "react";
+import {useRouter} from "next/router";
+import {NextIntlProvider} from "next-intl";
+import ServerErrorComponent from "@/components/errors/500";
+
+export default function Custom500() {
+ const router = useRouter()
+ const [messages, setMessages] = useState(null)
+ useEffect(() => {
+ const fetch = async () => {
+ const tempMessages = (await import(`&/locales/${router.locale}/app.json`)).default
+ setMessages(tempMessages)
+ }
+ fetch()
+ }, []);
+
+ if (!messages) return
+
+ return (
+
+
+
+ );
+}
diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx
new file mode 100644
index 0000000..4cd2a1d
--- /dev/null
+++ b/src/pages/_app.jsx
@@ -0,0 +1,34 @@
+import "&/fontiran.css";
+import AppLayout from "@/layouts/AppLayout";
+import MuiLayout from "@/layouts/MuiLayout";
+import {LanguageProvider} from "@/lib/app/contexts/language";
+import {LoadingProvider} from "@/lib/app/contexts/loading";
+import {UserProvider} from "@/lib/app/contexts/user";
+import "moment/locale/fa";
+import {NextIntlProvider} from "next-intl";
+import TitlePage from "@/core/components/TitlePage";
+import {SocketProvider} from "@/lib/app/contexts/socket";
+
+const App = ({Component, pageProps}) => {
+
+ return (<>
+
+
+
+
+ {pageProps.messages ? : ''}
+
+
+
+
+
+
+
+
+
+
+
+ >)
+};
+
+export default App;
diff --git a/src/pages/_document.jsx b/src/pages/_document.jsx
new file mode 100644
index 0000000..474e29b
--- /dev/null
+++ b/src/pages/_document.jsx
@@ -0,0 +1,61 @@
+import {createEmotionCacheRtl} from "@/core/utils/createEmotionCache";
+import theme from "@/core/utils/theme";
+import createEmotionServer from "@emotion/server/create-instance";
+import Document, {Head, Html, Main, NextScript} from "next/document";
+
+export default function MyDocument(props) {
+ const {emotionStyleTags} = props;
+
+ return (
+
+
+
+
+
+
+ {emotionStyleTags}
+
+
+
+
+
+
+ );
+}
+
+MyDocument.getInitialProps = async (ctx) => {
+ const originalRenderPage = ctx.renderPage;
+ let cache;
+ switch (ctx.locale) {
+ case "fa":
+ cache = createEmotionCacheRtl();
+ break;
+ }
+
+ const {extractCriticalToChunks} = createEmotionServer(cache);
+
+ ctx.renderPage = () =>
+ originalRenderPage({
+ enhanceApp: (App) =>
+ function EnhanceApp(props) {
+ return ;
+ },
+ });
+
+ const initialProps = await Document.getInitialProps(ctx);
+
+ const emotionStyles = extractCriticalToChunks(initialProps.html);
+ const emotionStyleTags = emotionStyles.styles.map((style) => (
+
+ ));
+
+ return {
+ ...initialProps,
+ emotionStyleTags,
+ };
+};
diff --git a/src/pages/dashboard/change-password/index.jsx b/src/pages/dashboard/change-password/index.jsx
new file mode 100644
index 0000000..f744382
--- /dev/null
+++ b/src/pages/dashboard/change-password/index.jsx
@@ -0,0 +1,23 @@
+import DashboardChangePasswordComponent from "@/components/dashboard/change-password";
+import WithAuthMiddleware from "@/middlewares/WithAuth";
+import {parse} from "next-useragent";
+
+export default function LoanFollowUp() {
+ return (
+
+
+
+ );
+}
+
+export async function getServerSideProps({req, locale}) {
+ const {isBot} = parse(req.headers["user-agent"]);
+ return {
+ props: {
+ messages: (await import(`&/locales/${locale}/app.json`)).default,
+ title: "Dashboard.change_password",
+ isBot,
+ locale
+ },
+ };
+}
diff --git a/src/pages/dashboard/edit-profile/index.jsx b/src/pages/dashboard/edit-profile/index.jsx
new file mode 100644
index 0000000..dfa2eec
--- /dev/null
+++ b/src/pages/dashboard/edit-profile/index.jsx
@@ -0,0 +1,23 @@
+import DashboardEditProfile from "@/components/dashboard/edit-profile";
+import WithAuthMiddleware from "@/middlewares/WithAuth";
+import {parse} from "next-useragent";
+
+export default function LoanFollowUp() {
+ return (
+
+
+
+ );
+}
+
+export async function getServerSideProps({req, locale}) {
+ const {isBot} = parse(req.headers["user-agent"]);
+ return {
+ props: {
+ messages: (await import(`&/locales/${locale}/app.json`)).default,
+ title: "Dashboard.edit_profile",
+ isBot,
+ locale
+ },
+ };
+}
diff --git a/src/pages/dashboard/expert-management/index.jsx b/src/pages/dashboard/expert-management/index.jsx
new file mode 100644
index 0000000..60dc6e2
--- /dev/null
+++ b/src/pages/dashboard/expert-management/index.jsx
@@ -0,0 +1,23 @@
+import WithAuthMiddleware from "@/middlewares/WithAuth";
+import {parse} from "next-useragent";
+import DashboardExpertManagementComponent from "@/components/dashboard/expert-management";
+
+export default function ExpertManagement() {
+ return (
+
+
+
+ );
+}
+
+export async function getServerSideProps({req, locale}) {
+ const {isBot} = parse(req.headers["user-agent"]);
+ return {
+ props: {
+ messages: (await import(`&/locales/${locale}/app.json`)).default,
+ title: "Dashboard.expert_management",
+ isBot,
+ locale
+ },
+ };
+}
diff --git a/src/pages/dashboard/index.jsx b/src/pages/dashboard/index.jsx
new file mode 100644
index 0000000..eed56eb
--- /dev/null
+++ b/src/pages/dashboard/index.jsx
@@ -0,0 +1,23 @@
+import DashboardFirstComponent from "@/components/dashboard/first";
+import WithAuthMiddleware from "@/middlewares/WithAuth";
+import {parse} from "next-useragent";
+
+export default function Dashboard() {
+ return (
+
+
+
+ );
+}
+
+export async function getServerSideProps({req, locale}) {
+ const {isBot} = parse(req.headers["user-agent"]);
+ return {
+ props: {
+ messages: (await import(`&/locales/${locale}/app.json`)).default,
+ title: "Dashboard.dashboard_page",
+ isBot,
+ locale
+ },
+ };
+}
diff --git a/src/pages/dashboard/role-management/index.jsx b/src/pages/dashboard/role-management/index.jsx
new file mode 100644
index 0000000..c309729
--- /dev/null
+++ b/src/pages/dashboard/role-management/index.jsx
@@ -0,0 +1,26 @@
+import RolePermissionMiddleware from "@/middlewares/RolePermission";
+import WithAuthMiddleware from "@/middlewares/WithAuth";
+import {parse} from "next-useragent";
+import DashboardRoleManagementComponent from "@/components/dashboard/role-management";
+
+const requiredPermissions = ["manage_roles"];
+export default function RoleManagement() {
+ return (
+
+
+
+
+
+ );
+}
+
+export async function getServerSideProps({req, locale}) {
+ const {isBot} = parse(req.headers["user-agent"]);
+ return {
+ props: {
+ messages: (await import(`&/locales/${locale}/app.json`)).default,
+ title: "Dashboard.role_management_page",
+ isBot,
+ },
+ };
+}
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
new file mode 100644
index 0000000..996ba03
--- /dev/null
+++ b/src/pages/index.jsx
@@ -0,0 +1,22 @@
+import FirstComponent from "@/components/first";
+import {parse} from "next-useragent";
+
+export default function Home() {
+ return (
+ <>
+
+ >
+ );
+}
+
+export async function getServerSideProps({req, locale}) {
+ const {isBot} = parse(req.headers["user-agent"]);
+ return {
+ props: {
+ messages: (await import(`&/locales/${locale}/app.json`)).default,
+ title: "first_page",
+ isBot,
+ locale
+ },
+ };
+}
diff --git a/src/pages/login-expert.jsx b/src/pages/login-expert.jsx
new file mode 100644
index 0000000..393f916
--- /dev/null
+++ b/src/pages/login-expert.jsx
@@ -0,0 +1,23 @@
+import LoginComponent from "@/components/login-expert";
+import WithoutAuthMiddleware from "@/middlewares/WithoutAuth";
+import {parse} from "next-useragent";
+
+export default function Login() {
+ return (
+
+
+
+ );
+}
+
+export async function getServerSideProps({req, locale}) {
+ const {isBot} = parse(req.headers["user-agent"]);
+ return {
+ props: {
+ messages: (await import(`&/locales/${locale}/app.json`)).default,
+ title: "Titles.title_login_expert_page",
+ isBot,
+ locale
+ },
+ };
+}