diff --git a/api-cronjob/Dockerfile b/api-cronjob/Dockerfile new file mode 100644 index 0000000..606072c --- /dev/null +++ b/api-cronjob/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.9-slim + +WORKDIR /app + +COPY script.py . + +RUN apt-get update && apt-get -y install curl + +CMD [ "python", "script.py" ] \ No newline at end of file diff --git a/cope2n-ai-fi/modules/sdsvkvu b/cope2n-ai-fi/modules/sdsvkvu index 6907ea0..b6d4fab 160000 --- a/cope2n-ai-fi/modules/sdsvkvu +++ b/cope2n-ai-fi/modules/sdsvkvu @@ -1 +1 @@ -Subproject commit 6907ea0183b141e3b4f3c21758c9123f1e9b2a27 +Subproject commit b6d4fab46f7f8689dd6b050cfbff2faa6a6f3fec diff --git a/cope2n-api/fwd/settings.py b/cope2n-api/fwd/settings.py index e2ddf3a..dd5801c 100755 --- a/cope2n-api/fwd/settings.py +++ b/cope2n-api/fwd/settings.py @@ -143,8 +143,8 @@ LANGUAGE_CODE = "en-us" USE_I18N = True CELERY_ENABLE_UTC = False -CELERY_TIMEZONE = "Asia/Ho_Chi_Minh" -TIME_ZONE = "Asia/Ho_Chi_Minh" +CELERY_TIMEZONE = "Asia/Singapore" +TIME_ZONE = "Asia/Singapore" USE_TZ = True # Static files (CSS, JavaScript, Images) @@ -220,6 +220,20 @@ SIZE_TO_COMPRESS = 2 * 1024 * 1024 MAX_NUMBER_OF_TEMPLATE = 3 MAX_PAGES_OF_PDF_FILE = 50 +OVERVIEW_REFRESH_INTERVAL = 2 +OVERVIEW_REPORT_ROOT = "overview" +OVERVIEW_REPORT_DURATION = ["30d", "7d"] + +SUBS = { + "SEAU": "AU", + "SESP": "SG", + "SME": "MY", + "SEPCO": "PH", + "TSE": "TH", + "SEIN": "ID", + "ALL": "all" + } + CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', diff --git a/cope2n-api/fwd_api/api/accuracy_view.py b/cope2n-api/fwd_api/api/accuracy_view.py index 301ba29..27caeae 100644 --- a/cope2n-api/fwd_api/api/accuracy_view.py +++ b/cope2n-api/fwd_api/api/accuracy_view.py @@ -14,9 +14,12 @@ import json from ..exception.exceptions import InvalidException, RequiredFieldException, NotFoundException from ..models import SubscriptionRequest, Report, ReportFile, SubscriptionRequestFile from ..utils.accuracy import shadow_report, MonthReportAccumulate, first_of_list, extract_report_detail_list, IterAvg -from ..utils.file import download_from_S3 +from ..utils.file import download_from_S3, convert_date_string +from ..utils.redis import RedisUtils from ..utils.process import string_to_boolean -from ..celery_worker.client_connector import c_connector +from ..utils.subsidiary import map_subsidiary_long_to_short, map_subsidiary_short_to_long + +redis_client = RedisUtils() class AccuracyViewSet(viewsets.ViewSet): lookup_field = "username" @@ -226,6 +229,12 @@ class AccuracyViewSet(viewsets.ViewSet): description='Subsidiary', type=OpenApiTypes.STR, ), + OpenApiParameter( + name='report_overview_duration', + location=OpenApiParameter.QUERY, + description=f'open of {settings.OVERVIEW_REPORT_DURATION}', + type=OpenApiTypes.STR, + ), ], responses=None, tags=['Accuracy'] ) @@ -240,12 +249,26 @@ class AccuracyViewSet(viewsets.ViewSet): include_test = string_to_boolean(request.GET.get('include_test', "false")) subsidiary = request.GET.get("subsidiary", "all") is_daily_report = string_to_boolean(request.GET.get('is_daily_report', "false")) - - try: - start_date = timezone.datetime.strptime(start_date_str, '%Y-%m-%dT%H:%M:%S%z') - end_date = timezone.datetime.strptime(end_date_str, '%Y-%m-%dT%H:%M:%S%z') - except ValueError: - raise InvalidException(excArgs="Date format") + report_overview_duration = request.GET.get("report_overview_duration", "") + subsidiary = map_subsidiary_long_to_short(subsidiary) + + if is_daily_report: + if report_overview_duration not in settings.OVERVIEW_REPORT_DURATION: + raise InvalidException(excArgs="overview duration") + end_date = timezone.now() + if report_overview_duration == "30d": + start_date = end_date - timezone.timedelta(days=30) + else: + start_date = end_date - timezone.timedelta(days=7) + start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0) + start_date_str = start_date.strftime('%Y-%m-%dT%H:%M:%S%z') + end_date_str = end_date.strftime('%Y-%m-%dT%H:%M:%S%z') + else: + try: + start_date = timezone.datetime.strptime(start_date_str, '%Y-%m-%dT%H:%M:%S%z') + end_date = timezone.datetime.strptime(end_date_str, '%Y-%m-%dT%H:%M:%S%z') + except ValueError: + raise InvalidException(excArgs="Date format") query_set = {"start_date_str": start_date_str, "end_date_str": end_date_str, @@ -255,7 +278,11 @@ class AccuracyViewSet(viewsets.ViewSet): "include_test": include_test, "subsidiary": subsidiary, "is_daily_report": is_daily_report, + "report_overview_duration": report_overview_duration } + # if is_daily_report: + # if (end_date-start_date) > timezone.timedelta(days=1): + # raise InvalidException(excArgs="Date range") report_id = "report" + "_" + timezone.datetime.now().strftime("%Y%m%d%H%M%S%z") + "_" + uuid.uuid4().hex new_report: Report = Report( @@ -268,8 +295,6 @@ class AccuracyViewSet(viewsets.ViewSet): end_at=end_date, status="Processing", ) - if is_daily_report: - new_report.created_at = end_date new_report.save() # Background job to calculate accuracy shadow_report(report_id, query_set) @@ -318,7 +343,7 @@ class AccuracyViewSet(viewsets.ViewSet): response = { 'report_detail': data, - 'metadata': {"subsidiary": report.subsidiary, + 'metadata': {"subsidiary": map_subsidiary_short_to_long(report.subsidiary), "start_at": report.start_at, "end_at": report.end_at}, 'page': { @@ -380,7 +405,7 @@ class AccuracyViewSet(viewsets.ViewSet): page_size = int(request.GET.get('page_size', 10)) if not start_date_str or not end_date_str: - reports = Report.objects.all() + reports = Report.objects.all().order_by('created_at').reverse() else: try: start_date = timezone.datetime.strptime(start_date_str, '%Y-%m-%dT%H:%M:%S%z') @@ -390,26 +415,35 @@ class AccuracyViewSet(viewsets.ViewSet): base_query = Q(created_at__range=(start_date, end_date)) if daily_report_only: base_query &= Q(is_daily_report=True) - reports = Report.objects.filter(base_query).order_by('created_at') + reports = Report.objects.filter(base_query).order_by('created_at').reverse() - paginator = Paginator(reports, page_size) page = paginator.get_page(page_number) + data = [] for report in page: + acc_keys = ["purchase_date", "retailername", "imei_number", "avg"] + acc = {} + for key in acc_keys: + fb = report.feedback_accuracy.get(key, 0) if report.feedback_accuracy else 0 + rv = report.reviewed_accuracy.get(key, 0) if report.reviewed_accuracy else 0 + acc[key] = max([fb, rv]) data.append({ "ID": report.id, "Created Date": report.created_at, + "Start Date": report.start_at, + "End Date": report.end_at, "No. Requests": report.number_request, "Status": report.status, - "Purchase Date Acc": report.reviewed_accuracy.get("purchase_date", None) if report.reviewed_accuracy else None, - "Retailer Acc": report.feedback_accuracy.get("retailername", None) if report.reviewed_accuracy else None, - "IMEI Acc": report.feedback_accuracy.get("imei_number", None) if report.reviewed_accuracy else None, - "Avg. Accuracy": report.feedback_accuracy.get("avg", None) if report.reviewed_accuracy else None, + "Purchase Date Acc": acc["purchase_date"], + "Retailer Acc": acc["retailername"], + "IMEI Acc": acc["imei_number"], + "Avg. Accuracy": acc["avg"], "Avg. Client Request Time": report.average_client_time.get("avg", 0) if report.average_client_time else 0, "Avg. OCR Processing Time": report.average_OCR_time.get("avg", 0) if report.average_OCR_time else 0, "report_id": report.report_id, + "Subsidiary": map_subsidiary_short_to_long(report.subsidiary), }) response = { @@ -427,103 +461,79 @@ class AccuracyViewSet(viewsets.ViewSet): @extend_schema( parameters=[ OpenApiParameter( - name='start_date', + name='duration', location=OpenApiParameter.QUERY, - description='Start date (YYYY-mm-DDTHH:MM:SSZ)', - type=OpenApiTypes.DATE, - default='2023-01-02T00:00:00+0700', - ), - OpenApiParameter( - name='end_date', - location=OpenApiParameter.QUERY, - description='End date (YYYY-mm-DDTHH:MM:SSZ)', - type=OpenApiTypes.DATE, - default='2024-01-10T00:00:00+0700', + description='one of [30d, 7d]', + type=OpenApiTypes.STR, + default='30d', ), OpenApiParameter( name='subsidiary', location=OpenApiParameter.QUERY, description='Subsidiary', type=OpenApiTypes.STR, - ), - OpenApiParameter( - name='page', - location=OpenApiParameter.QUERY, - description='Page number', - type=OpenApiTypes.INT, - required=False - ), - OpenApiParameter( - name='page_size', - location=OpenApiParameter.QUERY, - description='Number of items per page', - type=OpenApiTypes.INT, - required=False - ), + ) ], responses=None, tags=['Accuracy'] ) @action(detail=False, url_path="overview", methods=["GET"]) def overview(self, request): if request.method == 'GET': - subsidiary = request.GET.get('subsidiary', None) - start_date_str = request.GET.get('start_date', "") - end_date_str = request.GET.get('end_date', "") - page_number = int(request.GET.get('page', 1)) - page_size = int(request.GET.get('page_size', 10)) + subsidiary = request.GET.get('subsidiary', "ALL") + duration = request.GET.get('duration', "") - base_query = Q() - - if start_date_str and end_date_str: - try: - start_date = timezone.datetime.strptime(start_date_str, '%Y-%m-%dT%H:%M:%S%z') - end_date = timezone.datetime.strptime(end_date_str, '%Y-%m-%dT%H:%M:%S%z') - except ValueError: - raise InvalidException(excArgs="Date format") - base_query &= Q(created_at__range=(start_date, end_date)) - - if subsidiary: - base_query &= Q(subsidiary=subsidiary) - base_query &= Q(is_daily_report=True) - reports = Report.objects.filter(base_query).order_by('created_at') - - paginator = Paginator(reports, page_size) - page = paginator.get_page(page_number) - - data = [] - this_month_report = MonthReportAccumulate() - for report in page: - res = this_month_report.add(report) - if not(res): - _, _data, total = this_month_report() - data += [total] - data += _data - this_month_report = MonthReportAccumulate() - this_month_report.add(report) - else: - continue - _, _data, total = this_month_report() - data += [total] - data += _data - # Generate xlsx file - # workbook = dict2xlsx(data, _type="report") - # tmp_file = f"/tmp/{str(uuid.uuid4())}.xlsx" - # os.makedirs(os.path.dirname(tmp_file), exist_ok=True) - # workbook.save(tmp_file) - # c_connector.remove_local_file((tmp_file, "fake_request_id")) + subsidiary = map_subsidiary_long_to_short(subsidiary) + # Retrive data from Redis + key = f"{subsidiary}_{duration}" + data = json.loads(redis_client.get_specific_cache(settings.OVERVIEW_REPORT_ROOT, key)).get("data", []) response = { - # 'file': load_xlsx_file(), 'overview_data': data, - 'page': { - 'number': page.number, - 'total_pages': page.paginator.num_pages, - 'count': page.paginator.count, - } } return JsonResponse(response, status=200) return JsonResponse({'error': 'Invalid request method.'}, status=405) + + @extend_schema( + parameters=[ + OpenApiParameter( + name='duration', + location=OpenApiParameter.QUERY, + description='one of [30d, 7d]', + type=OpenApiTypes.STR, + default='30d', + ), + OpenApiParameter( + name='subsidiary', + location=OpenApiParameter.QUERY, + description='Subsidiary', + type=OpenApiTypes.STR, + ) + ], + responses=None, tags=['Accuracy'] + ) + @action(detail=False, url_path="overview_download_file", methods=["GET"]) + def overview_download_file(self, request): + if request.method == 'GET': + subsidiary = request.GET.get('subsidiary', "ALL") + duration = request.GET.get('duration', "") + + subsidiary = map_subsidiary_long_to_short(subsidiary) + + s3_key = f"{subsidiary}_{duration}.xlsx" + + tmp_file = "/tmp/" + s3_key + os.makedirs("/tmp", exist_ok=True) + download_from_S3("report/" + settings.OVERVIEW_REPORT_ROOT + "/" + s3_key, tmp_file) + file = open(tmp_file, 'rb') + response = FileResponse(file, status=200) + + # Set the content type and content disposition headers + response['Content-Type'] = 'application/octet-stream' + response['Content-Disposition'] = 'attachment; filename="{0}"'.format(os.path.basename(tmp_file)) + return response + + return JsonResponse({'error': 'Invalid request method.'}, status=405) @extend_schema( parameters=[], @@ -541,7 +551,7 @@ class AccuracyViewSet(viewsets.ViewSet): raise NotFoundException(excArgs=f"report: {report_id}") report = Report.objects.filter(report_id=report_id).first() # download from s3 to local - tmp_file = "/tmp/" + "report_" + uuid.uuid4().hex + ".xlsx" + tmp_file = "/tmp/" + report.subsidiary + "_" + report.start_at.strftime("%Y%m%d") + "_" + report.end_at.strftime("%Y%m%d") + "_created_on_" + report.created_at.strftime("%Y%m%d") + ".xlsx" os.makedirs("/tmp", exist_ok=True) if not report.S3_file_name: raise NotFoundException(excArgs="S3 file name") diff --git a/cope2n-api/fwd_api/celery_worker/client_connector.py b/cope2n-api/fwd_api/celery_worker/client_connector.py index c10cbdd..5394c8e 100755 --- a/cope2n-api/fwd_api/celery_worker/client_connector.py +++ b/cope2n-api/fwd_api/celery_worker/client_connector.py @@ -36,6 +36,8 @@ class CeleryConnector: 'remove_local_file': {'queue': "remove_local_file"}, 'csv_feedback': {'queue': "csv_feedback"}, 'make_a_report': {'queue': "report"}, + 'make_a_report_2': {'queue': "report_2"}, + } app = Celery( @@ -45,12 +47,16 @@ class CeleryConnector: ) def make_a_report(self, args): return self.send_task('make_a_report', args) + + def make_a_report_2(self, args): + return self.send_task('make_a_report_2', args) + def csv_feedback(self, args): return self.send_task('csv_feedback', args) def do_pdf(self, args): return self.send_task('do_pdf', args) - def upload_file_to_s3(self, args): - return self.send_task('upload_file_to_s3', args) + def upload_feedback_to_s3(self, args): + return self.send_task('upload_feedback_to_s3', args) def upload_file_to_s3(self, args): return self.send_task('upload_file_to_s3', args) def upload_report_to_s3(self, args): @@ -59,6 +65,7 @@ class CeleryConnector: return self.send_task('upload_obj_to_s3', args) def remove_local_file(self, args): return self.send_task('remove_local_file', args, countdown=280) # nearest execution of this task in 280 seconds + def process_fi(self, args): return self.send_task('process_fi_invoice', args) def process_fi_result(self, args): diff --git a/cope2n-api/fwd_api/celery_worker/internal_task.py b/cope2n-api/fwd_api/celery_worker/internal_task.py index bf12b3f..dc5e7cd 100755 --- a/cope2n-api/fwd_api/celery_worker/internal_task.py +++ b/cope2n-api/fwd_api/celery_worker/internal_task.py @@ -13,10 +13,13 @@ from fwd_api.models import SubscriptionRequestFile, FeedbackRequest, Report from ..utils import file as FileUtils from ..utils import process as ProcessUtil from ..utils import s3 as S3Util +from ..utils.accuracy import validate_feedback_file from fwd_api.constant.common import ProcessType import csv import json +import copy +from fwd_api.utils.accuracy import predict_result_to_ready from celery.utils.log import get_task_logger from fwd import settings @@ -79,6 +82,7 @@ def process_csv_feedback(csv_file_path, feedback_id): continue else: sub_rq = sub_rqs[0] + images = SubscriptionRequestFile.objects.filter(request=sub_rq) fb = {} # update user result (with validate) redemption_id = row.get('redemptionNumber') @@ -99,6 +103,42 @@ def process_csv_feedback(csv_file_path, feedback_id): if len(redemption_id) > 0: sub_rq.redemption_id = redemption_id sub_rq.save() + # Update files + time_cost = {"imei": [], "invoice": [], "all": []} + imei_count = 0 + if sub_rq.ai_inference_profile is None: + time_cost["imei"] = [-1 for _ in range(len(images))] + time_cost["invoice"] = [-1] + time_cost["all"] = [-1] + else: + for k, v in sub_rq.ai_inference_profile.items(): + time_cost[k.split("_")[0]].append(v["inference"][1][0] - v["inference"][0] + (v["postprocess"][1]-v["postprocess"][0])) + for i, image in enumerate(images): + _predict_result = copy.deepcopy(predict_result_to_ready(sub_rq.predict_result)) + _feedback_result = copy.deepcopy(sub_rq.feedback_result) + _reviewed_result = copy.deepcopy(sub_rq.reviewed_result) + image.processing_time = time_cost.get(image.doc_type, [0 for _ in range(image.index_in_request)])[image.index_in_request] + if not validate_feedback_file(_feedback_result, _predict_result): + status[request_id] = "Missalign imei number between feedback and predict" + continue + if image.doc_type == "invoice": + _predict_result["imei_number"] = [] + if _feedback_result: + _feedback_result["imei_number"] = [] + else: + None + if _reviewed_result: + _reviewed_result["imei_number"] = [] + else: + None + else: + _predict_result = {"retailername": None, "sold_to_party": None, "purchase_date": [], "imei_number": [_predict_result["imei_number"][image.index_in_request]]} + _feedback_result = {"retailername": None, "sold_to_party": None, "purchase_date": None, "imei_number": [_feedback_result["imei_number"][image.index_in_request]]} if _feedback_result else None + _reviewed_result = {"retailername": None, "sold_to_party": None, "purchase_date": None, "imei_number": [_reviewed_result["imei_number"][image.index_in_request]]} if _reviewed_result else None + image.predict_result = _predict_result + image.feedback_result = _feedback_result + image.reviewed_result = _reviewed_result + image.save() # update log into database feedback_rq = FeedbackRequest.objects.filter(feedback_id=feedback_id).first() feedback_rq.error_status = status diff --git a/cope2n-api/fwd_api/celery_worker/process_report_tasks.py b/cope2n-api/fwd_api/celery_worker/process_report_tasks.py index 5f72781..28a1e06 100644 --- a/cope2n-api/fwd_api/celery_worker/process_report_tasks.py +++ b/cope2n-api/fwd_api/celery_worker/process_report_tasks.py @@ -3,14 +3,19 @@ import traceback from fwd_api.models import SubscriptionRequest, Report, ReportFile from fwd_api.celery_worker.worker import app from ..utils import s3 as S3Util -from ..utils.accuracy import update_temp_accuracy, IterAvg, calculate_and_save_subcription_file, count_transactions, extract_report_detail_list +from ..utils.accuracy import update_temp_accuracy, IterAvg, calculate_and_save_subcription_file, count_transactions, extract_report_detail_list, calculate_a_request, ReportAccumulateByRequest from ..utils.file import dict2xlsx, save_workbook_file, save_report_to_S3 +from ..utils import time_stuff +from ..utils.redis import RedisUtils from django.utils import timezone from django.db.models import Q +import json +import copy from celery.utils.log import get_task_logger from fwd import settings +redis_client = RedisUtils() logger = get_task_logger(__name__) @@ -29,6 +34,7 @@ def mean_list(l): @app.task(name='make_a_report') def make_a_report(report_id, query_set): + # TODO: to be deprecated try: start_date = timezone.datetime.strptime(query_set["start_date_str"], '%Y-%m-%dT%H:%M:%S%z') end_date = timezone.datetime.strptime(query_set["end_date_str"], '%Y-%m-%dT%H:%M:%S%z') @@ -105,7 +111,7 @@ def make_a_report(report_id, query_set): errors += request_att["err"] num_request += 1 - transaction_att = count_transactions(start_date, end_date) + transaction_att = count_transactions(start_date, end_date, report.subsidiary) # Do saving process report.number_request = num_request report.number_images = number_images @@ -151,4 +157,155 @@ def make_a_report(report_id, query_set): except Exception as e: print("[ERROR]: an error occured while processing report: ", report_id) traceback.print_exc() - return 400 \ No newline at end of file + return 400 + +@app.task(name='make_a_report_2') +def make_a_report_2(report_id, query_set): + try: + start_date = timezone.datetime.strptime(query_set["start_date_str"], '%Y-%m-%dT%H:%M:%S%z') + end_date = timezone.datetime.strptime(query_set["end_date_str"], '%Y-%m-%dT%H:%M:%S%z') + base_query = Q(created_at__range=(start_date, end_date)) + if query_set["request_id"]: + base_query &= Q(request_id=query_set["request_id"]) + if query_set["redemption_id"]: + base_query &= Q(redemption_id=query_set["redemption_id"]) + base_query &= Q(is_test_request=False) + if isinstance(query_set["include_test"], str): + query_set["include_test"] = True if query_set["include_test"].lower() in ["true", "yes", "1"] else False + if query_set["include_test"]: + # base_query = ~base_query + base_query.children = base_query.children[:-1] + + elif isinstance(query_set["include_test"], bool): + if query_set["include_test"]: + base_query = ~base_query + if isinstance(query_set["subsidiary"], str): + if query_set["subsidiary"] and query_set["subsidiary"].lower().replace(" ", "")!="all": + base_query &= Q(redemption_id__startswith=query_set["subsidiary"]) + if isinstance(query_set["is_reviewed"], str): + if query_set["is_reviewed"] == "reviewed": + base_query &= Q(is_reviewed=True) + elif query_set["is_reviewed"] == "not reviewed": + base_query &= Q(is_reviewed=False) + # elif query_set["is_reviewed"] == "all": + # pass + + errors = [] + # Create a placeholder to fill + accuracy = {"feedback" :{"imei_number": IterAvg(), + "purchase_date": IterAvg(), + "retailername": IterAvg(), + "sold_to_party": IterAvg(),}, + "reviewed" :{"imei_number": IterAvg(), + "purchase_date": IterAvg(), + "retailername": IterAvg(), + "sold_to_party": IterAvg(),} + } # {"imei": {"acc": 0.1, count: 1}, ...} + time_cost = {"invoice": IterAvg(), + "imei": IterAvg()} + number_images = 0 + number_bad_images = 0 + # TODO: Multithreading + # Calculate accuracy, processing time, ....Then save. + subscription_requests = SubscriptionRequest.objects.filter(base_query).order_by('created_at') + report: Report = \ + Report.objects.filter(report_id=report_id).first() + # TODO: number of transaction by doc type + num_request = 0 + report_files = [] + report_engine = ReportAccumulateByRequest(report.subsidiary) + for request in subscription_requests: + if request.status != 200 or not (request.reviewed_result or request.feedback_result): + # Failed requests or lack of reviewed_result/feedback_result + continue + request_att, _report_files = calculate_a_request(report, request) + report_files += _report_files + report_engine.add(request, _report_files) + request.feedback_accuracy = {"imei_number" : mean_list(request_att["acc"]["feedback"].get("imei_number", [None])), + "purchase_date" : mean_list(request_att["acc"]["feedback"].get("purchase_date", [None])), + "retailername" : mean_list(request_att["acc"]["feedback"].get("retailername", [None])), + "sold_to_party" : mean_list(request_att["acc"]["feedback"].get("sold_to_party", [None]))} + request.reviewed_accuracy = {"imei_number" : mean_list(request_att["acc"]["reviewed"].get("imei_number", [None])), + "purchase_date" : mean_list(request_att["acc"]["reviewed"].get("purchase_date", [None])), + "retailername" : mean_list(request_att["acc"]["reviewed"].get("retailername", [None])), + "sold_to_party" : mean_list(request_att["acc"]["reviewed"].get("sold_to_party", [None]))} + request.save() + number_images += request_att["total_images"] + number_bad_images += request_att["bad_images"] + update_temp_accuracy(accuracy["feedback"], request_att["acc"]["feedback"], keys=["imei_number", "purchase_date", "retailername", "sold_to_party"]) + update_temp_accuracy(accuracy["reviewed"], request_att["acc"]["reviewed"], keys=["imei_number", "purchase_date", "retailername", "sold_to_party"]) + + time_cost["imei"].add(request_att["time_cost"].get("imei", [])) + time_cost["invoice"].add(request_att["time_cost"].get("invoice", [])) + + errors += request_att["err"] + num_request += 1 + + report_fine_data, _save_data = report_engine.save(report.report_id, query_set.get("is_daily_report", False), query_set["include_test"]) + transaction_att = count_transactions(start_date, end_date, report.subsidiary) + # Do saving process + report.number_request = num_request + report.number_images = number_images + report.number_imei = time_cost["imei"].count + report.number_invoice = time_cost["invoice"].count + report.number_bad_images = number_bad_images + # FIXME: refactor this data stream for endurability + report.average_OCR_time = {"invoice": time_cost["invoice"](), "imei": time_cost["imei"](), + "invoice_count": time_cost["invoice"].count, "imei_count": time_cost["imei"].count} + + report.average_OCR_time["avg"] = (report.average_OCR_time["invoice"]*report.average_OCR_time["invoice_count"] + report.average_OCR_time["imei"]*report.average_OCR_time["imei_count"])/(report.average_OCR_time["imei_count"] + report.average_OCR_time["invoice_count"]) if (report.average_OCR_time["imei_count"] + report.average_OCR_time["invoice_count"]) > 0 else None + + report.number_imei_transaction = transaction_att.get("imei", 0) + report.number_invoice_transaction = transaction_att.get("invoice", 0) + + acumulated_acc = {"feedback": {}, + "reviewed": {}} + for acc_type in ["feedback", "reviewed"]: + avg_acc = IterAvg() + for key in ["imei_number", "purchase_date", "retailername", "sold_to_party"]: + acumulated_acc[acc_type][key] = accuracy[acc_type][key]() + acumulated_acc[acc_type][key+"_count"] = accuracy[acc_type][key].count + avg_acc.add_avg(acumulated_acc[acc_type][key], acumulated_acc[acc_type][key+"_count"]) + acumulated_acc[acc_type]["avg"] = avg_acc() + + report.feedback_accuracy = acumulated_acc["feedback"] + report.reviewed_accuracy = acumulated_acc["reviewed"] + + report.errors = "|".join(errors) + report.status = "Ready" + report.save() + # Saving a xlsx file + report_files = ReportFile.objects.filter(report=report) + data = extract_report_detail_list(report_files, lower=True) + data_workbook = dict2xlsx(data, _type='report_detail') + local_workbook = save_workbook_file(report.report_id + ".xlsx", report, data_workbook) + s3_key=save_report_to_S3(report.report_id, local_workbook) + if query_set["is_daily_report"]: + # Save overview dashboard + # multiple accuracy by 100 + save_data = copy.deepcopy(_save_data) + for i, dat in enumerate(report_fine_data): + keys = [x for x in list(dat.keys()) if "accuracy" in x.lower()] + keys_percent = "images_quality" + for x_key in report_fine_data[i][keys_percent].keys(): + if "percent" not in x_key: + continue + report_fine_data[i][keys_percent][x_key] = report_fine_data[i][keys_percent][x_key]*100 + for key in keys: + if report_fine_data[i][key]: + for x_key in report_fine_data[i][key].keys(): + report_fine_data[i][key][x_key] = report_fine_data[i][key][x_key]*100 + data_workbook = dict2xlsx(report_fine_data, _type='report') + overview_filename = query_set["subsidiary"] + "_" + query_set["report_overview_duration"] + ".xlsx" + local_workbook = save_workbook_file(overview_filename, report, data_workbook, settings.OVERVIEW_REPORT_ROOT) + s3_key=save_report_to_S3(report.report_id, local_workbook) + redis_client.set_cache(settings.OVERVIEW_REPORT_ROOT, overview_filename.replace(".xlsx", ""), json.dumps(save_data)) + + except IndexError as e: + print(e) + traceback.print_exc() + print("NotFound request by report id, %d", report_id) + except Exception as e: + print("[ERROR]: an error occured while processing report: ", report_id) + traceback.print_exc() + return 400 diff --git a/cope2n-api/fwd_api/celery_worker/worker.py b/cope2n-api/fwd_api/celery_worker/worker.py index 5bb6963..31ad456 100755 --- a/cope2n-api/fwd_api/celery_worker/worker.py +++ b/cope2n-api/fwd_api/celery_worker/worker.py @@ -42,7 +42,7 @@ app.conf.update({ Queue('remove_local_file'), Queue('csv_feedback'), Queue('report'), - + Queue('report_2'), ], 'task_routes': { 'process_sap_invoice_result': {'queue': 'invoice_sap_rs'}, @@ -61,6 +61,7 @@ app.conf.update({ 'remove_local_file': {'queue': "remove_local_file"}, 'csv_feedback': {'queue': "csv_feedback"}, 'make_a_report': {'queue': "report"}, + 'make_a_report_2': {'queue': "report_2"}, } }) diff --git a/cope2n-api/fwd_api/migrations/0179_reportfile_is_bad_image.py b/cope2n-api/fwd_api/migrations/0179_reportfile_is_bad_image.py new file mode 100644 index 0000000..72f95a7 --- /dev/null +++ b/cope2n-api/fwd_api/migrations/0179_reportfile_is_bad_image.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.3 on 2024-02-04 23:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fwd_api', '0178_alter_reportfile_acc'), + ] + + operations = [ + migrations.AddField( + model_name='reportfile', + name='is_bad_image', + field=models.BooleanField(default=False), + ), + ] diff --git a/cope2n-api/fwd_api/migrations/0180_alter_reportfile_time_cost.py b/cope2n-api/fwd_api/migrations/0180_alter_reportfile_time_cost.py new file mode 100644 index 0000000..a646220 --- /dev/null +++ b/cope2n-api/fwd_api/migrations/0180_alter_reportfile_time_cost.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.3 on 2024-02-05 02:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fwd_api', '0179_reportfile_is_bad_image'), + ] + + operations = [ + migrations.AlterField( + model_name='reportfile', + name='time_cost', + field=models.FloatField(default=None, null=True), + ), + ] diff --git a/cope2n-api/fwd_api/models/ReportFile.py b/cope2n-api/fwd_api/models/ReportFile.py index 86e9270..9599d5d 100644 --- a/cope2n-api/fwd_api/models/ReportFile.py +++ b/cope2n-api/fwd_api/models/ReportFile.py @@ -16,6 +16,7 @@ class ReportFile(models.Model): # Data S3_uploaded = models.BooleanField(default=False) doc_type = models.CharField(max_length=200) + is_bad_image = models.BooleanField(default=False) predict_result = models.JSONField(null=True) feedback_result = models.JSONField(null=True) @@ -25,7 +26,7 @@ class ReportFile(models.Model): reviewed_accuracy = models.JSONField(null=True) acc = models.FloatField(default=0, null=True) - time_cost = models.FloatField(default=0) + time_cost = models.FloatField(default=None, null=True) is_reviewed = models.CharField(default="NA", max_length=5) # NA, No, Yes bad_image_reason = models.TextField(default="") counter_measures = models.TextField(default="") diff --git a/cope2n-api/fwd_api/utils/accuracy.py b/cope2n-api/fwd_api/utils/accuracy.py index 3ba1efd..6d500fd 100644 --- a/cope2n-api/fwd_api/utils/accuracy.py +++ b/cope2n-api/fwd_api/utils/accuracy.py @@ -5,14 +5,307 @@ import copy from typing import Any from .ocr_utils.ocr_metrics import eval_ocr_metric from .ocr_utils.sbt_report import post_processing_str +import uuid from fwd_api.models import SubscriptionRequest, SubscriptionRequestFile, ReportFile from ..celery_worker.client_connector import c_connector +from ..utils.file import dict2xlsx, save_workbook_file, save_report_to_S3 from django.db.models import Q +from django.utils import timezone +import redis +from fwd import settings +from ..models import SubscriptionRequest, Report, ReportFile +import json BAD_THRESHOLD = 0.75 valid_keys = ["retailername", "sold_to_party", "purchase_date", "imei_number"] +class ReportAccumulateByRequest: + def __init__(self, sub): + # self.redis_client = redis.Redis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, decode_responses=True) + self.sub = sub + self.current_time = None + self.data = {} # {"month": [total, {"day": day_data}]} + self.total_format = { + 'subs': "+", + 'extraction_date': "Subtotal ()", + 'total_images': 0, + 'images_quality': { + 'successful': 0, + 'successful_percent': 0, + 'bad': 0, + 'bad_percent': 0 + }, + 'average_accuracy_rate': { + 'imei': IterAvg(), + 'purchase_date': IterAvg(), + 'retailer_name': IterAvg(), + 'sold_to_party': IterAvg() + }, + 'average_processing_time': { + 'imei': IterAvg(), + 'invoice': IterAvg() + }, + 'usage': { + 'imei':0, + 'invoice': 0, + 'request': 0 + }, + 'feedback_accuracy': { + 'imei_number': IterAvg(), + 'purchase_date': IterAvg(), + 'retailername': IterAvg(), + 'sold_to_party': IterAvg() + }, + 'reviewed_accuracy': { + 'imei_number': IterAvg(), + 'purchase_date': IterAvg(), + 'retailername': IterAvg(), + 'sold_to_party': IterAvg() + }, + 'num_request': 0 + } + self.day_format = { + 'subs': sub, + 'extraction_date': "", + 'num_imei': 0, + 'num_invoice': 0, + 'total_images': 0, + 'images_quality': { + 'successful': 0, + 'successful_percent': 0, + 'bad': 0, + 'bad_percent': 0 + }, + 'average_accuracy_rate': { + 'imei': IterAvg(), + 'purchase_date': IterAvg(), + 'retailer_name': IterAvg(), + 'sold_to_party': IterAvg() + }, + 'average_processing_time': { + 'imei': IterAvg(), + 'invoice': IterAvg() + }, + 'usage': { + 'imei': 0, + 'invoice': 0, + 'request': 0 + }, + 'feedback_accuracy': { + 'imei_number': IterAvg(), + 'purchase_date': IterAvg(), + 'retailername': IterAvg(), + 'sold_to_party': IterAvg() + }, + 'reviewed_accuracy': { + 'imei_number': IterAvg(), + 'purchase_date': IterAvg(), + 'retailername': IterAvg(), + 'sold_to_party': IterAvg() + }, + "report_files": [], + 'num_request': 0 + }, + + @staticmethod + def update_total(total, report_file): + total["total_images"] += 1 + total["images_quality"]["successful"] += 1 if not report_file.is_bad_image else 0 + total["images_quality"]["bad"] += 1 if report_file.is_bad_image else 0 + # total["report_files"].append(report_file) + + if sum([len(report_file.reviewed_accuracy[x]) for x in report_file.reviewed_accuracy.keys() if "_count" not in x]) > 0 : + total["average_accuracy_rate"]["imei"].add(report_file.reviewed_accuracy.get("imei_number", [])) + total["average_accuracy_rate"]["purchase_date"].add(report_file.reviewed_accuracy.get("purchase_date", [])) + total["average_accuracy_rate"]["retailer_name"].add(report_file.reviewed_accuracy.get("retailername", [])) + total["average_accuracy_rate"]["sold_to_party"].add(report_file.reviewed_accuracy.get("sold_to_party", [])) + elif sum([len(report_file.feedback_accuracy[x]) for x in report_file.feedback_accuracy.keys() if "_count" not in x]) > 0: + total["average_accuracy_rate"]["imei"].add(report_file.feedback_accuracy.get("imei_number", [])) + total["average_accuracy_rate"]["purchase_date"].add(report_file.feedback_accuracy.get("purchase_date", [])) + total["average_accuracy_rate"]["retailer_name"].add(report_file.feedback_accuracy.get("retailername", [])) + total["average_accuracy_rate"]["sold_to_party"].add(report_file.feedback_accuracy.get("sold_to_party", [])) + + for key in ["imei_number", "purchase_date", "retailername", "sold_to_party"]: + total["feedback_accuracy"][key].add(report_file.feedback_accuracy.get(key, [])) + for key in ["imei_number", "purchase_date", "retailername", "sold_to_party"]: + total["reviewed_accuracy"][key].add(report_file.reviewed_accuracy.get(key, [])) + + if not total["average_processing_time"].get(report_file.doc_type, None): + print(f"[WARM]: Weird doctype: {report_file.doc_type}") + total["average_processing_time"] = IterAvg() + total["average_processing_time"][report_file.doc_type].add_avg(report_file.time_cost, 1) if report_file.time_cost else 0 + + total["usage"]["imei"] += 1 if report_file.doc_type == "imei" else 0 + total["usage"]["invoice"] += 1 if report_file.doc_type == "invoice" else 0 + + return total + + @staticmethod + def update_day(day_data, report_file): + day_data["total_images"] += 1 + day_data["images_quality"]["successful"] += 1 if not report_file.is_bad_image else 0 + day_data["images_quality"]["bad"] += 1 if report_file.is_bad_image else 0 + day_data["num_imei"] += 1 if report_file.doc_type == "imei" else 0 + day_data["num_invoice"] += 1 if report_file.doc_type == "invoice" else 0 + day_data["report_files"].append(report_file) + + if sum([len(report_file.reviewed_accuracy[x]) for x in report_file.reviewed_accuracy.keys() if "_count" not in x]) > 0 : + day_data["average_accuracy_rate"]["imei"].add(report_file.reviewed_accuracy.get("imei_number", 0)) + day_data["average_accuracy_rate"]["purchase_date"].add(report_file.reviewed_accuracy.get("purchase_date", 0)) + day_data["average_accuracy_rate"]["retailer_name"].add(report_file.reviewed_accuracy.get("retailername", 0)) + day_data["average_accuracy_rate"]["sold_to_party"].add(report_file.reviewed_accuracy.get("sold_to_party", 0)) + elif sum([len(report_file.feedback_accuracy[x]) for x in report_file.feedback_accuracy.keys() if "_count" not in x]) > 0: + day_data["average_accuracy_rate"]["imei"].add(report_file.feedback_accuracy.get("imei_number", 0)) + day_data["average_accuracy_rate"]["purchase_date"].add(report_file.feedback_accuracy.get("purchase_date", 0)) + day_data["average_accuracy_rate"]["retailer_name"].add(report_file.feedback_accuracy.get("retailername", 0)) + day_data["average_accuracy_rate"]["sold_to_party"].add(report_file.feedback_accuracy.get("sold_to_party", 0)) + + for key in ["imei_number", "purchase_date", "retailername", "sold_to_party"]: + day_data["feedback_accuracy"][key].add(report_file.feedback_accuracy.get(key, 0)) + for key in ["imei_number", "purchase_date", "retailername", "sold_to_party"]: + day_data["reviewed_accuracy"][key].add(report_file.reviewed_accuracy.get(key, 0)) + + if not day_data["average_processing_time"].get(report_file.doc_type, None): + print(f"[WARM]: Weird doctype: {report_file.doc_type}") + day_data["average_processing_time"] = IterAvg() + day_data["average_processing_time"][report_file.doc_type].add_avg(report_file.time_cost, 1) if report_file.time_cost else 0 + + return day_data + + def add(self, request, report_files): + this_month = request.created_at.strftime("%Y%m") + this_day = request.created_at.strftime("%Y%m%d") + if not self.data.get(this_month, None): + self.data[this_month] = [copy.deepcopy(self.total_format), {}] + if not self.data[this_month][1].get(this_day, None): + self.data[this_month][1][this_day] = copy.deepcopy(self.day_format)[0] + self.data[this_month][1][this_day]['extraction_date'] = request.created_at.strftime("%Y-%m-%d") + usage = self.count_transactions_within_day(this_day) + self.data[this_month][1][this_day]["usage"]["imei"] = usage.get("imei", 0) + self.data[this_month][1][this_day]["usage"]["invoice"] = usage.get("invoice", 0) + self.data[this_month][1][this_day]["usage"]["request"] = usage.get("request", 0) + + self.data[this_month][1][this_day]['num_request'] += 1 + self.data[this_month][0]['num_request'] += 1 + for report_file in report_files: + self.data[this_month][0] = self.update_total(self.data[this_month][0], report_file) # Update the subtotal within the month + self.data[this_month][1][this_day] = self.update_day(self.data[this_month][1][this_day], report_file) # Update the subtotal of the day + + def count_transactions_within_day(self, date_string): + # convert this day into timezone.datetime at UTC + start_date = datetime.strptime(date_string, "%Y%m%d") + start_date_with_timezone = timezone.make_aware(start_date) + end_date_with_timezone = start_date_with_timezone + timezone.timedelta(days=1) + return count_transactions(start_date_with_timezone, end_date_with_timezone, self.sub) + + def save(self, root_report_id, is_daily_report=False, include_test=False): + report_data = self.get() + fine_data = [] + save_data = {"file": {"overview": f"{root_report_id}/{root_report_id}.xlsx"}, + "data": fine_data} # {"sub_report_id": "S3 location", "data": fine_data} + # extract data + for month in report_data.keys(): + fine_data.append(report_data[month][0]) + for day in report_data[month][1].keys(): + fine_data.append(report_data[month][1][day]) + # save daily reports + report_id = root_report_id + "_" + day + start_date = datetime.strptime(day, "%Y%m%d") + start_date_with_timezone = timezone.make_aware(start_date) + end_date_with_timezone = start_date_with_timezone + timezone.timedelta(days=1) + _average_OCR_time = {"invoice": self.data[month][1][day]["average_processing_time"]["invoice"](), "imei": self.data[month][1][day]["average_processing_time"]["imei"](), + "invoice_count": self.data[month][1][day]["average_processing_time"]["invoice"].count, "imei_count": self.data[month][1][day]["average_processing_time"]["imei"].count} + + _average_OCR_time["avg"] = (_average_OCR_time["invoice"]*_average_OCR_time["invoice_count"] + _average_OCR_time["imei"]*_average_OCR_time["imei_count"])/(_average_OCR_time["imei_count"] + _average_OCR_time["invoice_count"]) if (_average_OCR_time["imei_count"] + _average_OCR_time["invoice_count"]) > 0 else None + acumulated_acc = {"feedback_accuracy": {}, + "reviewed_accuracy": {}} + for acc_type in ["feedback_accuracy", "reviewed_accuracy"]: + avg_acc = IterAvg() + for key in ["imei_number", "purchase_date", "retailername", "sold_to_party"]: + acumulated_acc[acc_type][key] = self.data[month][1][day][acc_type][key]() + acumulated_acc[acc_type][key+"_count"] = self.data[month][1][day][acc_type][key].count + avg_acc.add_avg(acumulated_acc[acc_type][key], acumulated_acc[acc_type][key+"_count"]) + acumulated_acc[acc_type]["avg"] = avg_acc() + acumulated_acc[acc_type]["avg_count"] = avg_acc.count + new_report: Report = Report( + report_id=report_id, + is_daily_report=is_daily_report, + subsidiary=self.sub.lower().replace(" ", ""), + include_test=include_test, + start_at=start_date_with_timezone, + end_at=end_date_with_timezone, + status="Ready", + number_request=report_data[month][1][day]["num_request"], + number_images=report_data[month][1][day]["total_images"], + number_imei=report_data[month][1][day]["num_imei"], + number_invoice=report_data[month][1][day]["num_invoice"], + number_bad_images=report_data[month][1][day]["images_quality"]["bad"], + average_OCR_time=_average_OCR_time, + number_imei_transaction=report_data[month][1][day]["usage"]["imei"], + number_invoice_transaction=report_data[month][1][day]["usage"]["invoice"], + feedback_accuracy=acumulated_acc["feedback_accuracy"], + reviewed_accuracy=acumulated_acc["reviewed_accuracy"], + ) + new_report.save() + data = extract_report_detail_list(self.data[month][1][day]["report_files"], lower=True) + data_workbook = dict2xlsx(data, _type='report_detail') + local_workbook = save_workbook_file(report_id + ".xlsx", new_report, data_workbook) + s3_key=save_report_to_S3(report_id, local_workbook) + return fine_data, save_data + + def get(self) -> Any: + # FIXME: This looks like a junk + _data = copy.deepcopy(self.data) + for month in _data.keys(): + _data[month][0]["images_quality"]["successful_percent"] = _data[month][0]["images_quality"]["successful"]/_data[month][0]["total_images"] if _data[month][0]["total_images"] > 0 else 0 + _data[month][0]["images_quality"]["bad_percent"] = _data[month][0]["images_quality"]["bad"]/_data[month][0]["total_images"] if _data[month][0]["total_images"] > 0 else 0 + num_transaction_imei = 0 + num_transaction_invoice = 0 + for day in _data[month][1].keys(): + num_transaction_imei += _data[month][1][day]["usage"].get("imei", 0) + num_transaction_invoice += _data[month][1][day]["usage"].get("invoice", 0) + _data[month][1][day]["average_accuracy_rate"]["imei"] = _data[month][1][day]["average_accuracy_rate"]["imei"]() + _data[month][1][day]["average_accuracy_rate"]["purchase_date"] = _data[month][1][day]["average_accuracy_rate"]["purchase_date"]() + _data[month][1][day]["average_accuracy_rate"]["retailer_name"] = _data[month][1][day]["average_accuracy_rate"]["retailer_name"]() + _data[month][1][day]["average_accuracy_rate"]["sold_to_party"] = _data[month][1][day]["average_accuracy_rate"]["sold_to_party"]() + _data[month][1][day]["average_processing_time"]["imei"] = _data[month][1][day]["average_processing_time"]["imei"]() + _data[month][1][day]["average_processing_time"]["invoice"] = _data[month][1][day]["average_processing_time"]["invoice"]() + + _data[month][1][day]["feedback_accuracy"]["imei_number"] = _data[month][1][day]["feedback_accuracy"]["imei_number"]() + _data[month][1][day]["feedback_accuracy"]["purchase_date"] = _data[month][1][day]["feedback_accuracy"]["purchase_date"]() + _data[month][1][day]["feedback_accuracy"]["retailername"] = _data[month][1][day]["feedback_accuracy"]["retailername"]() + _data[month][1][day]["feedback_accuracy"]["sold_to_party"] = _data[month][1][day]["feedback_accuracy"]["sold_to_party"]() + _data[month][1][day]["reviewed_accuracy"]["imei_number"] = _data[month][1][day]["reviewed_accuracy"]["imei_number"]() + _data[month][1][day]["reviewed_accuracy"]["purchase_date"] = _data[month][1][day]["reviewed_accuracy"]["purchase_date"]() + _data[month][1][day]["reviewed_accuracy"]["retailername"] = _data[month][1][day]["reviewed_accuracy"]["retailername"]() + _data[month][1][day]["reviewed_accuracy"]["sold_to_party"] = _data[month][1][day]["reviewed_accuracy"]["sold_to_party"]() + _data[month][1][day].pop("report_files") + + _data[month][1][day]["images_quality"]["successful_percent"] = _data[month][1][day]["images_quality"]["successful"]/_data[month][1][day]["total_images"] if _data[month][1][day]["total_images"] > 0 else 0 + _data[month][1][day]["images_quality"]["bad_percent"] = _data[month][1][day]["images_quality"]["bad"]/_data[month][1][day]["total_images"] if _data[month][1][day]["total_images"] > 0 else 0 + + _data[month][0]["usage"]["imei"] = num_transaction_imei + _data[month][0]["usage"]["invoice"] = num_transaction_invoice + _data[month][0]["average_accuracy_rate"]["imei"] = _data[month][0]["average_accuracy_rate"]["imei"]() + _data[month][0]["average_accuracy_rate"]["purchase_date"] = _data[month][0]["average_accuracy_rate"]["purchase_date"]() + _data[month][0]["average_accuracy_rate"]["retailer_name"] = _data[month][0]["average_accuracy_rate"]["retailer_name"]() + _data[month][0]["average_accuracy_rate"]["sold_to_party"] = _data[month][0]["average_accuracy_rate"]["sold_to_party"]() + _data[month][0]["average_processing_time"]["imei"] = _data[month][0]["average_processing_time"]["imei"]() + _data[month][0]["average_processing_time"]["invoice"] = _data[month][0]["average_processing_time"]["invoice"]() + + _data[month][0]["feedback_accuracy"]["imei_number"] = _data[month][0]["feedback_accuracy"]["imei_number"]() + _data[month][0]["feedback_accuracy"]["purchase_date"] = _data[month][0]["feedback_accuracy"]["purchase_date"]() + _data[month][0]["feedback_accuracy"]["retailername"] = _data[month][0]["feedback_accuracy"]["retailername"]() + _data[month][0]["feedback_accuracy"]["sold_to_party"] = _data[month][0]["feedback_accuracy"]["sold_to_party"]() + _data[month][0]["reviewed_accuracy"]["imei_number"] = _data[month][0]["reviewed_accuracy"]["imei_number"]() + _data[month][0]["reviewed_accuracy"]["purchase_date"] = _data[month][0]["reviewed_accuracy"]["purchase_date"]() + _data[month][0]["reviewed_accuracy"]["retailername"] = _data[month][0]["reviewed_accuracy"]["retailername"]() + _data[month][0]["reviewed_accuracy"]["sold_to_party"] = _data[month][0]["reviewed_accuracy"]["sold_to_party"]() + + return _data + + class MonthReportAccumulate: def __init__(self): self.month = None @@ -89,7 +382,7 @@ class MonthReportAccumulate: self.total["usage"]["invoice"] += report.number_invoice_transaction def add(self, report): - report_month = report.created_at.month + report_month = report.start_at.month if self.month is None: self.month = report_month @@ -103,7 +396,7 @@ class MonthReportAccumulate: new_data = copy.deepcopy(self.data_format)[0] new_data["num_imei"] = report.number_imei new_data["subs"] = report.subsidiary - new_data["extraction_date"] = report.created_at + new_data["extraction_date"] = report.start_at new_data["num_invoice"] = report.number_invoice new_data["total_images"] = report.number_images new_data["images_quality"]["successful"] = report.number_images - report.number_bad_images @@ -130,10 +423,38 @@ class MonthReportAccumulate: self.accumulate(report) return True + def clear(self): + self.month = None + self.total = { + 'subs': "+", + 'extraction_date': "Subtotal ()", + 'total_images': 0, + 'images_quality': { + 'successful': 0, + 'successful_percent': 0, + 'bad': 0, + 'bad_percent': 0 + }, + 'average_accuracy_rate': { + 'imei': IterAvg(), + 'purchase_date': IterAvg(), + 'retailer_name': IterAvg() + }, + 'average_processing_time': { + 'imei': IterAvg(), + 'invoice': IterAvg() + }, + 'usage': { + 'imei':0, + 'invoice': 0 + } + } + self.data = [] + def __call__(self): - self.total["images_quality"]["successful_percent"] += self.total["images_quality"]["successful"]/self.total["total_images"] if self.total["total_images"] else 0 - self.total["images_quality"]["bad_percent"] += self.total["images_quality"]["bad"]/self.total["total_images"] if self.total["total_images"] else 0 total = copy.deepcopy(self.total) + total["images_quality"]["successful_percent"] = total["images_quality"]["successful"]/total["total_images"] if total["total_images"] else 0 + total["images_quality"]["bad_percent"] = total["images_quality"]["bad"]/total["total_images"] if total["total_images"] else 0 total["average_accuracy_rate"]["imei"] = total["average_accuracy_rate"]["imei"]() total["average_accuracy_rate"]["purchase_date"] = total["average_accuracy_rate"]["purchase_date"]() total["average_accuracy_rate"]["retailer_name"] = total["average_accuracy_rate"]["retailer_name"]() @@ -167,6 +488,16 @@ class IterAvg: def __call__(self): return self.avg +def validate_feedback_file(feedback, predict): + if feedback: + imei_feedback = feedback.get("imei_number", []) + imei_feedback = [x for x in imei_feedback if x != ""] + num_imei_feedback = len(imei_feedback) + num_imei_predict = len(predict.get("imei_number", [])) + if num_imei_feedback != num_imei_predict: + return False + return True + def first_of_list(the_list): if not the_list: return None @@ -210,9 +541,11 @@ def extract_report_detail_list(report_detail_list, lower=False, in_percent=True) data[i][key] = data[i][key]*100 return data -def count_transactions(start_date, end_date): +def count_transactions(start_date, end_date, subsidiary="all"): base_query = Q(created_at__range=(start_date, end_date)) base_query &= Q(is_test_request=False) + if subsidiary and subsidiary.lower().replace(" ", "")!="all": + base_query &= Q(redemption_id__startswith=subsidiary) transaction_att = {} print(f"[DEBUG]: atracting transactions attribute...") @@ -226,6 +559,10 @@ def count_transactions(start_date, end_date): transaction_att[doc_type] = 1 else: transaction_att[doc_type] += 1 + if not transaction_att.get("request", None): + transaction_att["request"] = 1 + else: + transaction_att["request"] += 1 return transaction_att def convert_datetime_format(date_string: str, is_gt=False) -> str: @@ -359,6 +696,7 @@ def calculate_and_save_subcription_file(report, request): reviewed_accuracy=att["acc"]["reviewed"], acc=att["avg_acc"], time_cost=image.processing_time, + is_bad_image=att["is_bad_image"], bad_image_reason=image.reason, counter_measures=image.counter_measures, error="|".join(att["err"]) @@ -387,6 +725,72 @@ def calculate_and_save_subcription_file(report, request): continue return request_att + +def calculate_a_request(report, request): + request_att = {"acc": {"feedback": {"imei_number": [], + "purchase_date": [], + "retailername": [], + "sold_to_party": [], + }, + "reviewed": {"imei_number": [], + "purchase_date": [], + "retailername": [], + "sold_to_party": [], + }}, + "err": [], + "time_cost": {}, + "total_images": 0, + "bad_images": 0} + images = SubscriptionRequestFile.objects.filter(request=request) + report_files = [] + for image in images: + status, att = calculate_subcription_file(image) + if status != 200: + continue + image.feedback_accuracy = att["acc"]["feedback"] + image.reviewed_accuracy = att["acc"]["reviewed"] + image.is_bad_image_quality = att["is_bad_image"] + image.save() + new_report_file = ReportFile(report=report, + correspond_request_id=request.request_id, + correspond_redemption_id=request.redemption_id, + doc_type=image.doc_type, + predict_result=image.predict_result, + feedback_result=image.feedback_result, + reviewed_result=image.reviewed_result, + feedback_accuracy=att["acc"]["feedback"], + reviewed_accuracy=att["acc"]["reviewed"], + acc=att["avg_acc"], + is_bad_image=att["is_bad_image"], + time_cost=image.processing_time, + bad_image_reason=image.reason, + counter_measures=image.counter_measures, + error="|".join(att["err"]) + ) + report_files.append(new_report_file) + if request_att["time_cost"].get(image.doc_type, None): + request_att["time_cost"][image.doc_type].append(image.processing_time) + else: + request_att["time_cost"][image.doc_type] = [image.processing_time] + try: + request_att["acc"]["feedback"]["imei_number"] += att["acc"]["feedback"]["imei_number"] + request_att["acc"]["feedback"]["purchase_date"] += att["acc"]["feedback"]["purchase_date"] + request_att["acc"]["feedback"]["retailername"] += att["acc"]["feedback"]["retailername"] + request_att["acc"]["feedback"]["sold_to_party"] += att["acc"]["feedback"]["sold_to_party"] + + request_att["acc"]["reviewed"]["imei_number"] += att["acc"]["reviewed"]["imei_number"] + request_att["acc"]["reviewed"]["purchase_date"] += att["acc"]["reviewed"]["purchase_date"] + request_att["acc"]["reviewed"]["retailername"] += att["acc"]["reviewed"]["retailername"] + request_att["acc"]["reviewed"]["sold_to_party"] += att["acc"]["reviewed"]["sold_to_party"] + + request_att["bad_images"] += int(att["is_bad_image"]) + request_att["total_images"] += 1 + request_att["err"] += att["err"] + except Exception as e: + print(e) + continue + + return request_att, report_files def calculate_subcription_file(subcription_request_file): @@ -490,5 +894,5 @@ def calculate_attributions(request): # for one request, return in order return acc, data, time_cost, image_quality_num, error def shadow_report(report_id, query): - c_connector.make_a_report( + c_connector.make_a_report_2( (report_id, query)) \ No newline at end of file diff --git a/cope2n-api/fwd_api/utils/file.py b/cope2n-api/fwd_api/utils/file.py index 3f44694..f7434d9 100644 --- a/cope2n-api/fwd_api/utils/file.py +++ b/cope2n-api/fwd_api/utils/file.py @@ -7,6 +7,7 @@ import json from PIL import Image, ExifTags from django.core.files.uploadedfile import TemporaryUploadedFile from django.utils import timezone +from datetime import datetime from fwd import settings from ..utils import s3 as S3Util @@ -30,6 +31,16 @@ s3_client = S3Util.MinioS3Client( bucket_name=settings.S3_BUCKET_NAME ) +def convert_date_string(date_string): + # Parse the input date string + date_format = "%Y-%m-%d %H:%M:%S.%f %z" + parsed_date = datetime.strptime(date_string, date_format) + + # Format the date as "YYYYMMDD" + formatted_date = parsed_date.strftime("%Y%m%d") + + return formatted_date + def validate_report_list(request): start_date_str = request.GET.get('start_date') end_date_str = request.GET.get('end_date') @@ -190,10 +201,13 @@ def save_feedback_file(file_name: str, rq: FeedbackRequest, uploaded_file: dict) csvfile.write(file_contents) return file_path -def save_workbook_file(file_name: str, rp: Report, workbook): +def save_workbook_file(file_name: str, rp: Report, workbook, prefix=""): report_id = str(rp.report_id) - folder_path = os.path.join(settings.MEDIA_ROOT, "report", report_id) + if not prefix: + folder_path = os.path.join(settings.MEDIA_ROOT, "report", report_id) + else: + folder_path = os.path.join(settings.MEDIA_ROOT, "report", prefix) os.makedirs(folder_path, exist_ok = True) file_path = os.path.join(folder_path, file_name) @@ -388,12 +402,17 @@ def build_media_url_v2(media_id: str, user_id: int, sub_id: int, u_sync_id: str) def get_value(_dict, keys): keys = keys.split('.') value = _dict - for key in keys: - if not key in value.keys(): - return "-" - else: - value = value.get(key, {}) - + try: + for key in keys: + if not key in value.keys(): + return "-" + else: + value = value.get(key, {}) + except Exception as e: + print(f"[ERROR]: {e}") + print(f"[ERROR]: value: {value}") + print(f"[ERROR]: keys: {keys}") + if not value: return "-" elif isinstance(value, list): @@ -475,13 +494,23 @@ def dict2xlsx(input: json, _type='report'): ws[key + str(start_index)].border = border if _type == 'report': - ws[key + str(start_index)].font = font_black_bold - if key_index == 0 or (key_index >= 9 and key_index <= 15): - ws[key + str(start_index)].fill = fill_gray - elif key_index == 1: - ws[key + str(start_index)].fill = fill_green - elif key_index >= 4 and key_index <= 8: - ws[key + str(start_index)].fill = fill_yellow + if subtotal['subs'] == '+': + ws[key + str(start_index)].font = font_black_bold + if key_index == 0 or (key_index >= 9 and key_index <= 15): + ws[key + str(start_index)].fill = fill_gray + elif key_index == 1: + ws[key + str(start_index)].fill = fill_green + elif key_index >= 4 and key_index <= 8: + ws[key + str(start_index)].fill = fill_yellow + else: + if 'average_accuracy_rate' in mapping[key] and type(value) in [int, float] and value < 95: + ws[key + str(start_index)].style = normal_cell_red + elif 'average_processing_time' in mapping[key] and type(value) in [int, float] and value > 2.0: + ws[key + str(start_index)].style = normal_cell_red + elif 'bad_percent' in mapping[key] and type(value) in [int, float] and value > 10: + ws[key + str(start_index)].style = normal_cell_red + else : + ws[key + str(start_index)].style = normal_cell elif _type == 'report_detail': if 'accuracy' in mapping[key] and type(value) in [int, float] and value < 75: ws[key + str(start_index)].style = normal_cell_red @@ -491,21 +520,5 @@ def dict2xlsx(input: json, _type='report'): ws[key + str(start_index)].style = normal_cell start_index += 1 - - if 'data' in subtotal.keys(): - for record in subtotal['data']: - for key in mapping.keys(): - value = get_value(record, mapping[key]) - ws[key + str(start_index)] = value - if 'average_accuracy_rate' in mapping[key] and type(value) in [int, float] and value < 95: - ws[key + str(start_index)].style = normal_cell_red - elif 'average_processing_time' in mapping[key] and type(value) in [int, float] and value > 2.0: - ws[key + str(start_index)].style = normal_cell_red - elif 'bad_percent' in mapping[key] and type(value) in [int, float] and value > 10: - ws[key + str(start_index)].style = normal_cell_red - else : - ws[key + str(start_index)].style = normal_cell - - start_index += 1 return wb diff --git a/cope2n-api/fwd_api/utils/redis.py b/cope2n-api/fwd_api/utils/redis.py index ff65035..d8d74e1 100644 --- a/cope2n-api/fwd_api/utils/redis.py +++ b/cope2n-api/fwd_api/utils/redis.py @@ -22,6 +22,9 @@ class RedisUtils: for key, value in self.redis_client.hgetall(request_id).items(): resutlt[key] = json.loads(value) return resutlt + + def get_specific_cache(self, request_id, key): + return json.loads(self.redis_client.hget(request_id, key)) def get_size(self, request_id): return self.redis_client.hlen(request_id) diff --git a/cope2n-api/fwd_api/utils/subsidiary.py b/cope2n-api/fwd_api/utils/subsidiary.py new file mode 100644 index 0000000..d10c879 --- /dev/null +++ b/cope2n-api/fwd_api/utils/subsidiary.py @@ -0,0 +1,11 @@ +from fwd.settings import SUBS + +def map_subsidiary_long_to_short(long_sub): + short_sub = SUBS.get(long_sub.upper(), "all") + return short_sub.upper() + +def map_subsidiary_short_to_long(short_sub): + for k, v in SUBS.items(): + if v == short_sub.upper(): + return k + return "ALL" \ No newline at end of file diff --git a/cope2n-api/fwd_api/utils/time_stuff.py b/cope2n-api/fwd_api/utils/time_stuff.py new file mode 100644 index 0000000..bbdf6cf --- /dev/null +++ b/cope2n-api/fwd_api/utils/time_stuff.py @@ -0,0 +1,9 @@ +def is_the_same_day(first_day, second_day): + if first_day.day == second_day.day and first_day.month == second_day.month and first_day.year == second_day.year: + return True + return False + +def is_the_same_month(first_day, second_day): + if first_day.month == second_day.month and first_day.year == second_day.year: + return True + return False \ No newline at end of file diff --git a/cope2n-api/scripts/script.py b/cope2n-api/scripts/script.py new file mode 100644 index 0000000..713c925 --- /dev/null +++ b/cope2n-api/scripts/script.py @@ -0,0 +1,68 @@ +import os +import time +import requests +from datetime import datetime + +# Get the proxy URL from the environment variable +interval = 60*60*1 # 1 minute +update_cost = 60*3 +proxy_url = os.getenv('PROXY', "localhost") + +# Define the login API URL +login_url = f'{proxy_url}/api/ctel/login/' +login_token = None + +# Define the login credentials +login_credentials = { + 'username': 'sbt', + 'password': '7Eg4AbWIXDnufgn' +} + +# Define the command to call the update API +update_url = f'{proxy_url}/api/ctel/make_report/' +update_params = { + 'is_daily_report': 'true', + 'report_overview_duration': '', + 'subsidiary': None +} + +"report_overview_duration" + +def update_report(login_token, report_overview_duration=["30d", "7d"], subsidiary=["all", "SEAU", "SESP", "SME", "SEPCO", "TSE", "SEIN"]): + headers = {'Authorization': login_token} + for dur in report_overview_duration: + for sub in subsidiary: + update_params["report_overview_duration"] = dur + update_params["subsidiary"] = sub + update_response = requests.get(update_url, params=update_params, headers=headers) + print("[INFO]: update_response at {} by {} - {} with status {}".format(datetime.now(), dur, sub, update_response.status_code)) + update_response.raise_for_status() + time.sleep(update_cost) + +# Define the interval in seconds between API calls +# time.sleep(60) + +while True: + # Call the login API and retrieve the login token + if not login_token: + login_response = requests.post(login_url, data=login_credentials) + # login_response.raise_for_status() + if login_response.status_code == 200: + login_token = login_response.json()['token'] + print("[INFO] relogged in at {}".format(datetime.now())) + + # Call the update API + try: + update_report(login_token) + except Exception as e: + print(f"[ERROR]: {e}") + print(f"[ERROR]: Failed to update_response, retrying...") + login_response = requests.post(login_url, data=login_credentials) + # login_response.raise_for_status() + if login_response.status_code == 200: + login_token = login_response.json()['token'] + print("[INFO] relogged in at {}".format(datetime.now())) + update_report(login_token) + + # Wait for the specified interval + time.sleep(interval) \ No newline at end of file diff --git a/cope2n-fe/Dockerfile b/cope2n-fe/Dockerfile new file mode 100644 index 0000000..f0b31ef --- /dev/null +++ b/cope2n-fe/Dockerfile @@ -0,0 +1,21 @@ +FROM node:21-alpine AS build + +WORKDIR /app/ +COPY --chown=node:node package*.json ./ +RUN npm install -g npm@10.4.0 && npm install +COPY --chown=node:node . . +RUN npm run build +RUN npm cache clean --force +USER node + +################### +# PRODUCTION +################### +FROM nginx:stable-alpine AS nginx + +COPY --from=build /app/dist/ /usr/share/nginx/html/ +COPY --from=build /app/run.sh /app/ +COPY --from=build /app/nginx.conf /configs/ +RUN chmod +x /app/run.sh + +CMD ["/app/run.sh"] diff --git a/cope2n-fe/nginx.conf b/cope2n-fe/nginx.conf new file mode 100644 index 0000000..33cf790 --- /dev/null +++ b/cope2n-fe/nginx.conf @@ -0,0 +1,35 @@ +server { + # listen {{port}}; + # listen [::]:{{port}}; + # server_name localhost; + client_max_body_size 100M; + + location ~ ^/api { + proxy_pass {{proxy_server}}; + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_send_timeout 300; + } + + location /static/drf_spectacular_sidecar/ { + alias /backend-static/drf_spectacular_sidecar/; + } + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri /index.html; + } + + location ~ ^/static/drf_spectacular_sidecar/swagger-ui-dist { + proxy_pass {{proxy_server}}; + } + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + +} \ No newline at end of file diff --git a/cope2n-fe/package-lock.json b/cope2n-fe/package-lock.json index 144140f..aa23e73 100644 --- a/cope2n-fe/package-lock.json +++ b/cope2n-fe/package-lock.json @@ -25,6 +25,7 @@ "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-json-view-lite": "^1.2.1", + "react-office-viewer": "^1.0.4", "react-router-dom": "^6.6.1", "styled-components": "^5.3.6", "uuid": "^9.0.0" @@ -3075,6 +3076,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@handsontable/react": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/@handsontable/react/-/react-12.4.0.tgz", + "integrity": "sha512-qCjZq5TuJTvYKIl3B111rw5X3vlmNckW7520izSJKdDOVxlPr6aLCr13yYL+0eFtBIMYl6M3logzkFFh7z72ew==", + "peerDependencies": { + "handsontable": ">=12.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -3223,6 +3232,16 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", @@ -4362,11 +4381,30 @@ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-2.0.3.tgz", "integrity": "sha512-jhAJzaanK5LqyLQ50jJNIrB8fjL9gwWZTgYjevPvkDLMU+kTAZkYsobI59nYoeSrH1PucuyJEi247Pb90t6XUg==" }, + "node_modules/@types/eslint": { + "version": "8.56.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", + "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", @@ -4395,8 +4433,7 @@ "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/json5": { "version": "0.0.29", @@ -4423,7 +4460,6 @@ "version": "18.19.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.12.tgz", "integrity": "sha512-uLcpWEAvatBEubmgCMzWforZbAu1dT9syweWnU3/DNwbeUBq2miP5nG8Y4JL9MDMKWt+7Yv1CSvA8xELdEl54w==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -4434,6 +4470,14 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "dev": true }, + "node_modules/@types/pikaday": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@types/pikaday/-/pikaday-1.7.4.tgz", + "integrity": "sha512-0KsHVyw5pTG829nqG4IRu7m+BFQlFEBdbE/1i3S5182HeKUKv1uEW0gyEmkJVp5i4IV+9pyh23O83+KpRkSQbw==", + "dependencies": { + "moment": ">=2.14.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", @@ -4859,11 +4903,181 @@ "vite": "^4.1.0-beta.0" } }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "peer": true + }, + "node_modules/@yiiran/get-file-type": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yiiran/get-file-type/-/get-file-type-1.0.3.tgz", + "integrity": "sha512-NPXIjasI3phA5sGTrEIdJ/DZlO7HG0NnoPmHXdxOkREyojt5ktw0UWhGRySffpO2SsW9VCUQdVgxyoOdnI+Uxg==" + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -4871,6 +5085,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "peer": true, + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -4890,11 +5113,18 @@ "node": ">=0.4.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4906,6 +5136,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peer": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/align-text": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", @@ -5416,7 +5655,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -5447,6 +5685,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/bignumber.js": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-8.1.1.tgz", + "integrity": "sha512-QD46ppGintwPGuL1KqmwhR0O+N2cZUg8JG/VzwI2e28sM9TqHjQB10lI4QAaMHVbLzwVLLAwEglpKPViWX+5NQ==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -5467,6 +5722,11 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5543,6 +5803,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "peer": true + }, "node_modules/call-bind": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", @@ -5616,6 +5882,18 @@ "node": ">=0.10.0" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -5646,6 +5924,15 @@ "pnpm": ">=7" } }, + "node_modules/chevrotain": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-6.5.0.tgz", + "integrity": "sha512-BwqQ/AgmKJ8jcMEjaSnfMybnKMgGTrtDKowfTP3pX4jwVy0kNjRsT/AP6h+wC3+3NC+X8X15VWBnTCQlX+wQFg==", + "optional": true, + "dependencies": { + "regexp-to-ast": "0.4.0" + } + }, "node_modules/chokidar": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", @@ -5667,6 +5954,15 @@ "fsevents": "~2.3.1" } }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -5745,6 +6041,14 @@ "node": ">=0.8" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -5836,6 +6140,16 @@ "toggle-selection": "^1.0.6" } }, + "node_modules/core-js": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.1.tgz", + "integrity": "sha512-IgdsbxNyMskrTFxa9lWHyMwAJU5gXOPP+1yO+K59d50VLVAIDAbs7gIv705KzALModfK3ZrSZTPNpC0PQgIZuw==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-js-compat": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", @@ -5849,6 +6163,11 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -5890,6 +6209,17 @@ "typescript": ">=4" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -6139,6 +6469,11 @@ "node": ">=0.3.1" } }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==" + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -6163,6 +6498,11 @@ "node": ">=6.0.0" } }, + "node_modules/dompurify": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz", + "integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==" + }, "node_modules/dotignore": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz", @@ -6174,6 +6514,14 @@ "ignored": "bin/ignored" } }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "dependencies": { + "underscore": "^1.13.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.651", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.651.tgz", @@ -6185,6 +6533,28 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -6280,6 +6650,12 @@ "safe-array-concat": "^1.0.1" } }, + "node_modules/es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "peer": true + }, "node_modules/es-set-tostringtag": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", @@ -6905,7 +7281,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -6917,7 +7292,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "engines": { "node": ">=4.0" } @@ -6937,6 +7311,15 @@ "node": ">=0.10.0" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -6954,8 +7337,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -6989,8 +7371,7 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -7202,6 +7583,14 @@ "node": ">= 6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -7357,6 +7746,12 @@ "node": ">= 6" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "peer": true + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -7419,8 +7814,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -7428,6 +7822,22 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/handsontable": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/handsontable/-/handsontable-12.4.0.tgz", + "integrity": "sha512-bivyvW41RKT3SNIo+f2SOgvV376NbtTkfWYYRCSTOW/bx6EnjH4S2Pl4vfVpFPRQLOFPGTEWniuXLg8b1VkpBQ==", + "dependencies": { + "@types/pikaday": "1.7.4", + "core-js": "^3.0.0", + "dompurify": "^2.1.1", + "moment": "2.29.4", + "numbro": "2.1.2", + "pikaday": "1.8.2" + }, + "optionalDependencies": { + "hyperformula": "^2.4.0" + } + }, "node_modules/has": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", @@ -7550,6 +7960,55 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/hyperformula": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/hyperformula/-/hyperformula-2.6.1.tgz", + "integrity": "sha512-GzL+R+UweB4FtT7p71cDS0wFVq5CMUjBincSSVzccBsPRgw4aIc/GbWLuuvrX6mH/r2ubYDu6udnr1/+OMoI9w==", + "optional": true, + "dependencies": { + "chevrotain": "^6.5.0", + "tiny-emitter": "^2.1.0", + "unorm": "^1.6.0" + } + }, + "node_modules/i18next": { + "version": "21.10.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.10.0.tgz", + "integrity": "sha512-YeuIBmFsGjUfO3qBmMOc0rQaun4mIpGKET5WDwvu8lU7gvwpcariZLNtL0Fzj+zazcHUrlXHiptcFhBMFaxzfg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.17.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.8.tgz", + "integrity": "sha512-Svm+MduCElO0Meqpj1kJAriTC6OhI41VhlT/A0UPjGoPZBhAHIaGE5EfsHlTpgdH09UVX7rcc72pSDDBeKSQQA==", + "dependencies": { + "@babel/runtime": "^7.19.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -7591,6 +8050,11 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/immutable": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", @@ -8304,6 +8768,44 @@ "node": ">=8" } }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8341,14 +8843,12 @@ "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -8413,6 +8913,49 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -8481,12 +9024,43 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "peer": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "peer": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8634,6 +9208,16 @@ "loose-envify": "cli.js" } }, + "node_modules/lop": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.1.tgz", + "integrity": "sha512-9xyho9why2A2tzm5aIcMWKvzqKsnxrf9B5I+8O30olh6lQU8PH978LqZoI4++37RBgS1Em5i54v1TFs/3wnmXQ==", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -8667,6 +9251,43 @@ "integrity": "sha512-/K3BC0KIsO+WK2i94LkMPv3wslMrazrQhfi5We9fMbLlLjzoOSJWr7TAdupLlDWaJcWxwoNosBkhFDejiu5VDw==", "dev": true }, + "node_modules/mammoth": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.6.0.tgz", + "integrity": "sha512-jOwbj6BwJzxCf6jr2l1zmSemniIkLnchvELXnDJCANlJawhzyIKObIq48B8kWEPLgUUh57k7FtEO3DHFQMnjMg==", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.1", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/mammoth/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "peer": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -8767,6 +9388,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, "node_modules/moo": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", @@ -8819,6 +9448,12 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "peer": true + }, "node_modules/node-gettext": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/node-gettext/-/node-gettext-3.0.0.tgz", @@ -8842,11 +9477,21 @@ "node": ">=0.10.0" } }, + "node_modules/numbro": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/numbro/-/numbro-2.1.2.tgz", + "integrity": "sha512-7w833BxZmKGLE9HI0aREtNVRVH6WTYUUlWf4qgA5gKNhPQ4F/MRZ14sc0v8eoLORprk9ZTVwYaLwj8N3Zgxwiw==", + "dependencies": { + "bignumber.js": "^8.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -9017,6 +9662,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==" + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -9175,6 +9825,11 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/papaparse": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", @@ -9261,6 +9916,14 @@ "resolved": "https://registry.npmjs.org/pdfast/-/pdfast-0.2.0.tgz", "integrity": "sha512-cq6TTu6qKSFUHwEahi68k/kqN2mfepjkGrG9Un70cgdRRKLKY6Rf8P8uvP2NvZktaQZNF3YE7agEkLj0vGK9bA==" }, + "node_modules/pdfjs-dist": { + "version": "2.10.377", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.10.377.tgz", + "integrity": "sha512-i0jRShtvgfsVQUNCoFYH4SVhPO3U0yhtiFLfZ0RR0B+68N+Vnwq+8B3cjWjLEwWGh8wg1XQ/sYMYKUlHn/Qpsw==", + "peerDependencies": { + "worker-loader": "^3.0.7" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -9277,6 +9940,11 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pikaday": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/pikaday/-/pikaday-1.8.2.tgz", + "integrity": "sha512-TNtsE+34BIax3WtkB/qqu5uepV1McKYEgvL3kWzU7aqPCpMEN6rBF3AOwu4WCwAealWlBGobXny/9kJb49C1ew==" + }, "node_modules/pkg-up": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", @@ -9501,11 +10169,15 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -9515,8 +10187,7 @@ "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/proxy-from-env": { "version": "1.1.0", @@ -9536,7 +10207,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -9575,6 +10245,15 @@ "integrity": "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==", "dev": true }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "peer": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/rc-cascader": { "version": "3.21.2", "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.21.2.tgz", @@ -10188,6 +10867,27 @@ "react": "^18.2.0" } }, + "node_modules/react-i18next": { + "version": "11.18.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz", + "integrity": "sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -10204,6 +10904,60 @@ "react": "^16.13.1 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-office-viewer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/react-office-viewer/-/react-office-viewer-1.0.4.tgz", + "integrity": "sha512-y998L8/K/DoPaksHXEYeNDdOij4OJr19NH/r7WytjddBC5KkDz3b8wU8C3oyyKb3cK5de//VOsJt5ptdNLUfFg==", + "dependencies": { + "@handsontable/react": "^12.1.3", + "@yiiran/get-file-type": "^1.0.3", + "handsontable": "^12.3.0", + "i18next": "^21.10.0", + "i18next-browser-languagedetector": "^6.1.8", + "mammoth": "^1.5.1", + "pdfjs-dist": "2.10.377", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "react-i18next": "^11.18.6", + "xlsx": "^0.18.5" + } + }, + "node_modules/react-office-viewer/node_modules/react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-office-viewer/node_modules/react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, + "node_modules/react-office-viewer/node_modules/scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -10321,6 +11075,12 @@ "@babel/runtime": "^7.8.4" } }, + "node_modules/regexp-to-ast": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.4.0.tgz", + "integrity": "sha512-4qf/7IsIKfSNHQXSwial1IFmfM1Cc/whNBQqRwe0V2stPe7KmN1U0tWQiIx6JiirgSrisjE0eECdNf7Tav1Ntw==", + "optional": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", @@ -10613,7 +11373,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -10684,6 +11443,24 @@ "loose-envify": "^1.1.0" } }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/scroll-into-view-if-needed": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", @@ -10700,6 +11477,15 @@ "semver": "bin/semver.js" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/set-function-length": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", @@ -10728,6 +11514,11 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", @@ -10824,6 +11615,22 @@ "node": ">=0.8.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -11045,6 +11852,15 @@ "react": "^16.11.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tape": { "version": "4.17.0", "resolved": "https://registry.npmjs.org/tape/-/tape-4.17.0.tgz", @@ -11082,6 +11898,83 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/terser": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", + "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "peer": true + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -11102,6 +11995,12 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "optional": true + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -11406,11 +12305,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -11461,6 +12364,15 @@ "node": ">= 10.0.0" } }, + "node_modules/unorm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", + "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==", + "optional": true, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -11494,7 +12406,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -11510,8 +12421,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/uuid": { "version": "9.0.1", @@ -11670,6 +12580,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", @@ -11678,6 +12596,19 @@ "loose-envify": "^1.0.0" } }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -11687,6 +12618,84 @@ "defaults": "^1.0.3" } }, + "node_modules/webpack": { + "version": "5.90.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz", + "integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11784,6 +12793,22 @@ "node": ">= 0.8.0" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/wordwrap": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", @@ -11792,6 +12817,26 @@ "node": ">=0.4.0" } }, + "node_modules/worker-loader": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", + "integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==", + "peer": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -11847,6 +12892,34 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/cope2n-fe/package.json b/cope2n-fe/package.json index 314d175..9213a6b 100644 --- a/cope2n-fe/package.json +++ b/cope2n-fe/package.json @@ -43,6 +43,7 @@ "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-json-view-lite": "^1.2.1", + "react-office-viewer": "^1.0.4", "react-router-dom": "^6.6.1", "styled-components": "^5.3.6", "uuid": "^9.0.0" diff --git a/cope2n-fe/run.sh b/cope2n-fe/run.sh new file mode 100644 index 0000000..fd8cab9 --- /dev/null +++ b/cope2n-fe/run.sh @@ -0,0 +1,5 @@ +#!/bin/sh +# update port and BD proxy +sed "s#{{proxy_server}}#$VITE_PROXY#g" /configs/nginx.conf > /etc/nginx/conf.d/default.conf +# run up +nginx -g 'daemon off;' \ No newline at end of file diff --git a/cope2n-fe/src/components/report-detail/report-overview-table.tsx b/cope2n-fe/src/components/report-detail/report-overview-table.tsx index 3f5f813..5f0f1c1 100644 --- a/cope2n-fe/src/components/report-detail/report-overview-table.tsx +++ b/cope2n-fe/src/components/report-detail/report-overview-table.tsx @@ -5,7 +5,7 @@ import React from 'react'; interface DataType { key: React.Key; subSidiaries: string; - extractionDate: string | Date; + extractionDate: string; snOrImeiNumber: number; invoiceNumber: number; totalImages: number; @@ -28,12 +28,37 @@ const columns: TableColumnsType = [ dataIndex: 'subSidiaries', key: 'subSidiaries', width: '100px', + render: (_, record) => { + if (record.subSidiaries === '+') return ''; + return String(record.subSidiaries).toUpperCase(); + }, + filters: [ + { text: 'ALL', value: 'ALL' }, + { text: 'SEAU', value: 'SEAU' }, + { text: 'SESP', value: 'SESP' }, + { text: 'SME', value: 'SME' }, + { text: 'SEPCO', value: 'SEPCO' }, + { text: 'TSE', value: 'TSE' }, + { text: 'SEIN', value: 'SEIN' }, + ], + filterMode: 'menu', + onFilter: (value: string, record) => record.subSidiaries.includes(String(value).toUpperCase()), }, { title: 'OCR extraction date', dataIndex: 'extractionDate', key: 'extractionDate', width: '130px', + render: (_, record) => { + if (record.extractionDate.includes('Subtotal')) + return ( + {record.extractionDate} + ); + return record.extractionDate; + }, + filters: [{ text: 'Subtotal', value: 'Subtotal' }], + filterMode: 'menu', + onFilter: (value: string, record) => record.extractionDate.includes(value), }, { title: 'OCR Images', @@ -73,7 +98,7 @@ const columns: TableColumnsType = [ key: 'successfulPercentage', width: '120px', render: (_, record) => { - return {(record.successfulPercentage * 100).toFixed(2)}; + return {(record.successfulPercentage * 100)?.toFixed(2)}; }, }, { @@ -91,7 +116,7 @@ const columns: TableColumnsType = [ const isAbnormal = record.badPercentage * 100 > 10; return ( - {(record.badPercentage * 100).toFixed(2)} + {(record.badPercentage * 100)?.toFixed(2)} ); }, @@ -108,10 +133,10 @@ const columns: TableColumnsType = [ key: 'snImeiAAR', width: '130px', render: (_, record) => { - const isAbnormal = record.snImeiAAR * 100 < 98; + const isAbnormal = record.snImeiAAR * 100 < 95; return ( - {(record.snImeiAAR * 100).toFixed(2)} + {(record.snImeiAAR * 100)?.toFixed(2)} ); }, @@ -139,7 +164,7 @@ const columns: TableColumnsType = [ const isAbnormal = record.retailerNameAAR * 100 < 98; return ( - {(record.retailerNameAAR * 100).toFixed(2)} + {(record.retailerNameAAR * 100)?.toFixed(2)} ); }, @@ -157,7 +182,7 @@ const columns: TableColumnsType = [ const isAbnormal = record.snImeiAPT > 2; return ( - {record.snImeiAPT.toFixed(2)} + {record?.snImeiAPT?.toFixed(2)} ); }, @@ -170,7 +195,7 @@ const columns: TableColumnsType = [ const isAbnormal = record.invoiceAPT > 2; return ( - {record.invoiceAPT.toFixed(2)} + {record?.invoiceAPT?.toFixed(2)} ); }, @@ -195,251 +220,39 @@ const columns: TableColumnsType = [ ]; interface ReportOverViewTableProps { - pagination: { - page: number; - page_size: number; - }; - setPagination: React.Dispatch< - React.SetStateAction<{ - page: number; - page_size: number; - }> - >; isLoading: boolean; data: any; } const ReportOverViewTable: React.FC = ({ - pagination, - setPagination, isLoading, data, }) => { - // const [pagination, setPagination] = useState({ - // page: 1, - // page_size: 10, - // }); - // const { isLoading, data } = useOverViewReport({ - // page: pagination.page, - // }); - - console.log('check >>>', pagination, isLoading, data); - const overviewDataResponse = data as any; - const dataSubsRows = overviewDataResponse?.overview_data - .map((item, index) => { - if (item.subs.includes('+')) { - return { - key: index, - subSidiaries: '', - extractionDate: item.extraction_date, - snOrImeiNumber: '', - invoiceNumber: '', - totalImages: item.total_images, - successfulNumber: item.images_quality.successful, - successfulPercentage: item.images_quality.successful_percent, - badNumber: item.images_quality.bad, - badPercentage: item.images_quality.bad_percent, - snImeiAAR: item.average_accuracy_rate.imei, - purchaseDateAAR: item.average_accuracy_rate.purchase_date, - retailerNameAAR: item.average_accuracy_rate.retailer_name, - snImeiAPT: item.average_processing_time.imei, - invoiceAPT: item.average_processing_time.invoice, - snImeiTC: item.usage.imei, - invoiceTC: item.usage.invoice, - }; - } else { - return null; - } - }) - .filter((item) => item); + const dataSubsRows = overviewDataResponse?.overview_data.map( + (item, index) => { + return { + key: index, + subSidiaries: item.subs, + extractionDate: item.extraction_date, + snOrImeiNumber: item.num_imei, + invoiceNumber: item.num_invoice, + totalImages: item.total_images, + successfulNumber: item.images_quality.successful, + successfulPercentage: item.images_quality.successful_percent, + badNumber: item.images_quality.bad, + badPercentage: item.images_quality.bad_percent, + snImeiAAR: item.average_accuracy_rate.imei, + purchaseDateAAR: item.average_accuracy_rate.purchase_date, + retailerNameAAR: item.average_accuracy_rate.retailer_name, + snImeiAPT: item.average_processing_time.imei, + invoiceAPT: item.average_processing_time.invoice, + snImeiTC: item.usage.imei, + invoiceTC: item.usage.invoice, + }; + }, + ); - const expandedRowRender = () => { - const subData = overviewDataResponse?.overview_data - .map((item, index) => { - if (!item.subs.includes('+')) { - return { - key: index, - subSidiaries: item.subs, - extractionDate: item.extraction_date, - snOrImeiNumber: item.num_imei, - invoiceNumber: item.num_invoice, - totalImages: item.total_images, - successfulNumber: item.images_quality.successful, - successfulPercentage: item.images_quality.successful_percent, - badNumber: item.images_quality.bad, - badPercentage: item.images_quality.bad_percent, - snImeiAAR: item.average_accuracy_rate.imei, - purchaseDateAAR: item.average_accuracy_rate.purchase_date, - retailerNameAAR: item.average_accuracy_rate.retailer_name, - snImeiAPT: item.average_processing_time.imei, - invoiceAPT: item.average_processing_time.invoice, - snImeiTC: item.usage.imei, - invoiceTC: item.usage.invoice, - }; - } else { - return null; - } - }) - .filter((item) => item); - - const subColumns: TableColumnsType = [ - { - title: 'Subs', - dataIndex: 'subSidiaries', - key: 'subSidiaries', - width: '100px', - }, - { - title: 'OCR extraction date', - dataIndex: 'extractionDate', - key: 'extractionDate', - width: '130px', - }, - { - title: 'SN/IMEI', - dataIndex: 'snOrImeiNumber', - key: 'snOrImeiNumber', - width: '50px', - }, - { - title: 'Invoice', - dataIndex: 'invoiceNumber', - key: 'invoiceNumber', - width: '50px', - }, - { - title: 'Total Images', - dataIndex: 'totalImages', - key: 'totalImages', - width: '130px', - }, - { - title: 'Successful', - dataIndex: 'successfulNumber', - key: 'successfulNumber', - width: '50px', - }, - { - title: '% Successful', - dataIndex: 'successfulPercentage', - key: 'successfulPercentage', - width: '120px', - render: (_, record) => { - return {(record.successfulPercentage * 100).toFixed(2)}; - }, - }, - { - title: 'Bad', - dataIndex: 'badNumber', - key: 'badNumber', - width: '30px', - }, - { - title: '% Bad', - dataIndex: 'badPercentage', - key: 'badPercentage', - width: '60px', - render: (_, record) => { - const isAbnormal = record.badPercentage * 100 > 10; - return ( - - {(record.badPercentage * 100).toFixed(2)} - - ); - }, - }, - - { - title: 'IMEI / Serial no.', - dataIndex: 'snImeiAAR', - key: 'snImeiAAR', - width: '130px', - render: (_, record) => { - const isAbnormal = record.snImeiAAR * 100 < 98; - return ( - - {(record.snImeiAAR * 100).toFixed(2)} - - ); - }, - }, - { - title: 'Purchase date', - dataIndex: 'purchaseDateAAR', - key: 'purchaseDateAAR', - width: '130px', - render: (_, record) => { - const isAbnormal = record.purchaseDateAAR * 100 < 98; - return ( - - {(record.purchaseDateAAR * 100).toFixed(2)} - - ); - }, - }, - { - title: 'Retailer name', - dataIndex: 'retailerNameAAR', - key: 'retailerNameAAR', - width: '130px', - render: (_, record) => { - const isAbnormal = record.retailerNameAAR * 100 < 98; - return ( - - {(record.retailerNameAAR * 100).toFixed(2)} - - ); - }, - }, - - { - title: 'SN/IMEI', - dataIndex: 'snImeiAPT', - key: 'snImeiAPT', - render: (_, record) => { - const isAbnormal = record.snImeiAPT > 2; - return ( - - {record.snImeiAPT.toFixed(2)} - - ); - }, - }, - { - title: 'Invoice', - dataIndex: 'invoiceAPT', - key: 'invoiceAPT', - render: (_, record) => { - const isAbnormal = record.invoiceAPT > 2; - return ( - - {record.invoiceAPT.toFixed(2)} - - ); - }, - }, - { - title: 'SN/IMEI', - dataIndex: 'snImeiTC', - key: 'snImeiTC', - }, - { - title: 'Invoice', - dataIndex: 'invoiceTC', - key: 'invoiceTC', - }, - ]; - return ( - - ); - }; return (
= ({ dataSource={dataSubsRows} bordered size='small' - expandable={{ expandedRowRender, defaultExpandedRowKeys: [0, 1] }} scroll={{ x: 2000 }} - pagination={{ - current: pagination.page, - pageSize: pagination.page_size, - total: overviewDataResponse?.page.count, - showTotal: (total, range) => - `${range[0]}-${range[1]} of ${total} items`, - onChange: (page, pageSize) => { - setPagination({ - page, - page_size: pageSize || 10, - }); - }, - showSizeChanger: false, - }} /> ); diff --git a/cope2n-fe/src/components/report-detail/report-table.tsx b/cope2n-fe/src/components/report-detail/report-table.tsx index 0a929cc..ae88e73 100644 --- a/cope2n-fe/src/components/report-detail/report-table.tsx +++ b/cope2n-fe/src/components/report-detail/report-table.tsx @@ -19,10 +19,10 @@ const ReportTable: React.FC = () => { })); const handleDownloadReport = async (report_id: string) => { - const reportFile = await downloadReport(report_id); + const {file, filename} = await downloadReport(report_id); const anchorElement = document.createElement('a'); - anchorElement.href = URL.createObjectURL(reportFile); - anchorElement.download = `${report_id}.xlsx`; // Set the desired new filename + anchorElement.href = URL.createObjectURL(file); + anchorElement.download = filename; document.body.appendChild(anchorElement); anchorElement.click(); @@ -37,12 +37,42 @@ const ReportTable: React.FC = () => { title: 'ID', dataIndex: 'ID', key: 'ID', - sorter: (a, b) => a.ID - b.ID, }, { - title: 'Created Date', + title: 'Report Date', dataIndex: 'Created Date', key: 'Created Date', + render: (_, record) => { + return {record['Created Date'].toString().split('T')[0]}; + }, + width: 110, + }, + { + title: 'Start Date', + dataIndex: 'Start Date', + key: 'Start Date', + render: (_, record) => { + return {record['Start Date'].toString().split('T')[0]}; + }, + width: 110, + }, + { + title: 'End Date', + dataIndex: 'End Date', + key: 'End Date', + render: (_, record) => { + return {record['End Date'].toString().split('T')[0]}; + }, + width: 110, + }, + { + title: 'Subsidiary', + dataIndex: 'Subsidiary', + key: 'Subsidiary', + render: (_, record) => { + return {String(record['Subsidiary']).toUpperCase()}; + }, + width: 110, }, { title: 'No. Requests', @@ -62,9 +92,12 @@ const ReportTable: React.FC = () => { dataIndex: 'Purchase Date Acc', key: 'Purchase Date Acc', render: (_, record) => { + const isAbnormal = record['Purchase Date Acc'] * 100 < 98; return ( - record['Purchase Date Acc'] && - Number(record['Purchase Date Acc']).toFixed(2) + + {record['Purchase Date Acc'] && + (Number(record['Purchase Date Acc']) * 100)?.toFixed(2)} + ); }, }, @@ -74,8 +107,12 @@ const ReportTable: React.FC = () => { dataIndex: 'Retailer Acc', key: 'Retailer Acc', render: (_, record) => { + const isAbnormal = record['Retailer Acc'] * 100 < 98; return ( - record['Retailer Acc'] && Number(record['Retailer Acc']).toFixed(2) + + {record['Retailer Acc'] && + (Number(record['Retailer Acc']) * 100)?.toFixed(2)} + ); }, }, @@ -84,38 +121,54 @@ const ReportTable: React.FC = () => { dataIndex: 'IMEI Acc', key: 'IMEI Acc', render: (_, record) => { - return record['IMEI Acc'] && Number(record['IMEI Acc']).toFixed(2); - }, - }, - { - title: 'Avg Accuracy', - dataIndex: 'Avg Accuracy', - key: 'Avg Accuracy', - render: (_, record) => { + const isAbnormal = record['IMEI Acc'] * 100 < 98; return ( - record['Avg Accuracy'] && Number(record['Avg Accuracy']).toFixed(2) + + {record['IMEI Acc'] && + (Number(record['IMEI Acc']) * 100)?.toFixed(2)} + ); }, }, { - title: 'Avg Client Request Time', - dataIndex: 'Avg. Client Request Time', - key: 'Avg. Client Request Time', + title: 'Avg. Accuracy', + dataIndex: 'Avg. Accuracy', + key: 'Avg. Accuracy', render: (_, record) => { + const isAbnormal = record['Avg. Accuracy'] * 100 < 98; return ( - record['Avg Client Request Time'] && - Number(record['Avg Client Request Time']).toFixed(2) + + {record['Avg. Accuracy'] && + (Number(record['Avg. Accuracy']) * 100)?.toFixed(2)} + ); }, }, + // { + // title: 'Avg Client Request Time', + // dataIndex: 'Avg. Client Request Time', + // key: 'Avg. Client Request Time', + // render: (_, record) => { + // const isAbnormal = record['Avg Client Request Time'] > 2; + // return ( + // + // {record['Avg Client Request Time'] && + // Number(record['Avg Client Request Time'])?.toFixed(2)} + // + // ); + // }, + // }, { title: 'Avg. OCR Processing Time', dataIndex: 'Avg. OCR Processing Time', key: 'Avg. OCR Processing Time', render: (_, record) => { + const isAbnormal = record['Avg. OCR Processing Time'] > 2; return ( - record['Avg. OCR Processing Time'] && - Number(record['Avg. OCR Processing Time']).toFixed(2) + + {record['Avg. OCR Processing Time'] && + Number(record['Avg. OCR Processing Time'])?.toFixed(2)} + ); }, }, @@ -123,7 +176,7 @@ const ReportTable: React.FC = () => { title: 'Actions', dataIndex: 'actions', key: 'actions', - width: 200, + width: 240, render: (_, record) => { return (
@@ -133,7 +186,7 @@ const ReportTable: React.FC = () => { }} style={{ marginRight: 10 }} > - Detail + Details */} - {/* */} + + } />
- + { + setSubsidiary(value); + }} /> - diff --git a/cope2n-fe/src/pages/inference/index.tsx b/cope2n-fe/src/pages/inference/index.tsx index 7bd18f1..82f24a4 100644 --- a/cope2n-fe/src/pages/inference/index.tsx +++ b/cope2n-fe/src/pages/inference/index.tsx @@ -64,6 +64,9 @@ const InferencePage = () => { +

+ {t`Upload files to process. The requests here will not be used in accuracy or payment calculations.`} +

diff --git a/cope2n-fe/src/pages/reports/index.tsx b/cope2n-fe/src/pages/reports/index.tsx index a4d8ec9..b2baa2c 100644 --- a/cope2n-fe/src/pages/reports/index.tsx +++ b/cope2n-fe/src/pages/reports/index.tsx @@ -105,9 +105,13 @@ const ReportsPage = () => { placeholder='Select a subsidiary' style={{ width: 200 }} options={[ - { value: 'all', label: 'ALL' }, - { value: 'sesp', label: 'SESP' }, - { value: 'seau', label: 'SEAU' }, + { value: 'ALL', label: 'ALL' }, + { value: 'SEAU', label: 'SEAU' }, + { value: 'SESP', label: 'SESP' }, + { value: 'SME', label: 'SME' }, + { value: 'SEPCO', label: 'SEPCO' }, + { value: 'TSE', label: 'TSE' }, + { value: 'SEIN', label: 'SEIN' }, ]} /> diff --git a/cope2n-fe/src/pages/reports/report_detail/index.tsx b/cope2n-fe/src/pages/reports/report_detail/index.tsx index 443cf51..d6ae296 100644 --- a/cope2n-fe/src/pages/reports/report_detail/index.tsx +++ b/cope2n-fe/src/pages/reports/report_detail/index.tsx @@ -1,10 +1,8 @@ -import { DownloadOutlined } from '@ant-design/icons'; +import { DownloadOutlined, ArrowLeftOutlined } from '@ant-design/icons'; import { t } from '@lingui/macro'; import { Button, Space, - Table, - TableColumnsType, Tooltip, Typography, } from 'antd'; @@ -12,10 +10,11 @@ import { SbtPageHeader } from 'components/page-header'; import { Dayjs } from 'dayjs'; import { ReportDetailList, ReportItemDetail } from 'models'; import { useReportDetailList } from 'queries/report'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { downloadReport } from 'request/report'; import styled from 'styled-components'; +import { SheetViewer } from "react-office-viewer" export interface ReportFormValues { dateRange: [Dayjs, Dayjs]; @@ -33,235 +32,24 @@ const HeaderContainer = styled(Space)` margin-bottom: 16px; `; -const ReportInformationContainer = styled.div` - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); - padding: 16px; - border-radius: 10px; - width: 100%; - height: 400px; -`; - -const columns: TableColumnsType = [ - { - title: 'Request ID', - dataIndex: 'Request ID', - key: 'Request ID', - width: 100, - render: (value, record, index) => { - const shortenedValue = - String(value).length > 20 - ? String(value).substring(0, 20) + '...' - : String(value); - return {shortenedValue}; - }, - }, - { - title: 'Redemption Number', - dataIndex: 'Redemption Number', - key: 'Redemption Number', - }, - { - title: 'Image type', - dataIndex: 'Image type', - key: 'Image type', - }, - { - title: 'IMEI_user submitted', - dataIndex: 'IMEI_user submitted', - key: 'IMEI_user submitted', - }, - { - title: 'IMEI_OCR retrieved', - dataIndex: 'IMEI_OCR retrieved', - key: 'IMEI_OCR retrieved', - }, - - { - title: 'IMEI1 Accuracy', - dataIndex: 'IMEI1 Accuracy', - key: 'IMEI1 Accuracy', - render: (_, record) => { - const isAbnormal = Number(record['IMEI1 Accuracy']) * 100 < 25; - return ( - - {record['IMEI1 Accuracy'] && - (Number(record['IMEI1 Accuracy']) * 100).toFixed(2)} - - ); - }, - }, - - { - title: 'Invoice_Purchase Date_Consumer', - dataIndex: 'Invoice_Purchase Date_Consumer', - key: 'Invoice_Purchase Date_Consumer', - }, - { - title: 'Invoice_Purchase Date_OCR', - dataIndex: 'Invoice_Purchase Date_OCR', - key: 'Invoice_Purchase Date_OCR', - }, - { - title: 'Invoice_Purchase Date Accuracy', - dataIndex: 'Invoice_Purchase Date Accuracy', - key: 'Invoice_Purchase Date Accuracy', - render: (_, record) => { - const isAbnormal = - Number(record['Invoice_Purchase Date Accuracy']) * 100 < 25; - return ( - - {record['Invoice_Purchase Date Accuracy'] && - (Number(record['Invoice_Purchase Date Accuracy']) * 100).toFixed(2)} - - ); - }, - }, - - { - title: 'Invoice_Retailer_Consumer', - dataIndex: 'Invoice_Retailer_Consumer', - key: 'Invoice_Retailer_Consumer', - }, - { - title: 'Invoice_Retailer_OCR', - dataIndex: 'Invoice_Retailer_OCR', - key: 'Invoice_Retailer_OCR', - }, - { - title: 'Invoice_Retailer Accuracy', - dataIndex: 'Invoice_Retailer Accuracy', - key: 'Invoice_Retailer Accuracy', - render: (_, record) => { - const isAbnormal = Number(record['Invoice_Retailer Accuracy']) * 100 < 25; - return ( - - {record['Invoice_Retailer Accuracy'] && - (Number(record['Invoice_Retailer Accuracy']) * 100).toFixed(2)} - - ); - }, - }, - { - title: 'Retailer_Revised Accuracy', - dataIndex: 'Retailer_Revised Accuracy', - key: 'Retailer_Revised Accuracy', - render: (_, record) => { - const isAbnormal = Number(record['Retailer_Revised Accuracy']) * 100 < 25; - return ( - - {record['Retailer_Revised Accuracy'] && - (Number(record['Retailer_Revised Accuracy']) * 100).toFixed(2)} - - ); - }, - }, - { - title: 'OCR Image Accuracy', - dataIndex: 'OCR Image Accuracy', - key: 'OCR Image Accuracy', - render: (_, record) => { - const isAbnormal = Number(record['OCR Image Accuracy']) * 100 < 25; - return ( - - {record['OCR Image Accuracy'] && - (Number(record['OCR Image Accuracy']) * 100).toFixed(2)} - - ); - }, - }, - { - title: 'OCR Image Speed (seconds)', - dataIndex: 'OCR Image Speed (seconds)', - key: 'OCR Image Speed (seconds)', - render: (_, record) => { - const isAbnormal = Number(record['OCR Image Speed (seconds)']) > 2; - return ( - - {record['OCR Image Speed (seconds)'] && - Number(record['OCR Image Speed (seconds)']).toFixed(2)} - - ); - }, - sorter: (a, b) => - a['OCR Image Speed (seconds)'] - b['OCR Image Speed (seconds)'], - }, - { - title: 'Reviewed', - dataIndex: 'Reviewed', - key: 'Reviewed', - }, - { - title: 'Bad Image Reasons', - dataIndex: 'Bad Image Reasons', - key: 'Bad Image Reasons', - }, - { - title: 'Countermeasures', - dataIndex: 'Countermeasures', - key: 'Countermeasures', - }, - { - title: 'IMEI_Revised Accuracy', - dataIndex: 'IMEI_Revised Accuracy', - key: 'IMEI_Revised Accuracy', - render: (_, record) => { - const isAbnormal = Number(record['IMEI_Revised Accuracy']) * 100 < 25; - return ( - - {record['IMEI_Revised Accuracy'] && - (Number(record['IMEI_Revised Accuracy']) * 100).toFixed(2)} - - ); - }, - }, - { - title: 'Purchase Date_Revised Accuracy', - dataIndex: 'Purchase Date_Revised Accuracy', - key: 'Purchase Date_Revised Accuracy', - render: (_, record) => { - const isAbnormal = - Number(record['Purchase Date_Revised Accuracy']) * 100 < 25; - return ( - - {record['Purchase Date_Revised Accuracy'] && - (Number(record['Purchase Date_Revised Accuracy']) * 100).toFixed(2)} - - ); - }, - }, - { - title: 'Retailer_Revised Accuracy', - dataIndex: 'Retailer_Revised Accuracy', - key: 'Retailer_Revised Accuracy', - render: (_, record) => { - const isAbnormal = Number(record['Retailer_Revised Accuracy']) * 100 < 25; - return ( - - {record['Retailer_Revised Accuracy'] && - (Number(record['Retailer_Revised Accuracy']) * 100).toFixed(2)} - - ); - }, - }, -]; - const ReportDetail = () => { + const [error, setError] = useState(null); + const [fileObject, setFileObject] = useState(null); const [pagination, setPagination] = useState({ page: 1, page_size: 10, }); const { id } = useParams<{ id: string }>(); - const { isLoading, data } = useReportDetailList({ report_id: id, page: pagination.page, }); const report_data = data as ReportDetailList; const handleDownloadReport = async () => { - const reportFile = await downloadReport(id); + const {file, filename} = await downloadReport(id); const anchorElement = document.createElement('a'); - anchorElement.href = URL.createObjectURL(reportFile); - anchorElement.download = `${id}.xlsx`; // Set the desired new filename + anchorElement.href = URL.createObjectURL(file); + anchorElement.download = filename; document.body.appendChild(anchorElement); anchorElement.click(); @@ -271,14 +59,52 @@ const ReportDetail = () => { URL.revokeObjectURL(anchorElement.href); }; + // Download and show report + useEffect(() => { + try { + downloadReport(id, (fileDetails) => { + if (!fileDetails?.file) { + setError("The report has not been ready to preview."); + } + var blob = new Blob( + [fileDetails.file], + {type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,"} + ); + let blobUrl = URL.createObjectURL(blob); + setFileObject(blobUrl); + }); + } catch (error) { + setError("The report has not been ready to preview."); + console.log(error); + } + }, []); + + const handleBack = () => { + window.history.back(); + }; + return ( <> {t`Report ${id.slice(0, 16)}...`} + <> + + + + {t`Report Details`} + } extra={
- `${range[0]}-${range[1]} of ${total} items`, - onChange: (page, pageSize) => { - setPagination({ - page, - page_size: pageSize || 10, - }); - }, - showSizeChanger: false, - }} - scroll={{ x: 2000 }} - /> + {(fileObject && !error) && } + {(!fileObject && !error) && Loading...} + {error && {error}} ); diff --git a/cope2n-fe/src/queries/report.ts b/cope2n-fe/src/queries/report.ts index 377aca1..fe3e58b 100644 --- a/cope2n-fe/src/queries/report.ts +++ b/cope2n-fe/src/queries/report.ts @@ -1,5 +1,5 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { ReportListParams } from 'models'; +import { ReportListParams, DashboardOverviewParams } from 'models'; import { getOverViewReport, getReportDetailList, @@ -42,7 +42,7 @@ export function useReportList(params?: ReportListParams, options?: any) { }); } -export function useOverViewReport(params?: ReportListParams, options?: any) { +export function useOverViewReport(params?: DashboardOverviewParams, options?: any) { return useQuery({ queryKey: ['overview-report', params], queryFn: () => getOverViewReport(params), diff --git a/cope2n-fe/src/request/report.ts b/cope2n-fe/src/request/report.ts index d068283..591ff38 100644 --- a/cope2n-fe/src/request/report.ts +++ b/cope2n-fe/src/request/report.ts @@ -7,6 +7,7 @@ import { ReportDetailListParams, ReportListParams, ReportListType, + DashboardOverviewParams, } from 'models'; import { API } from './api'; @@ -68,14 +69,11 @@ export async function getReportList(params?: ReportListParams) { } } -export async function getOverViewReport(params?: ReportListParams) { +export async function getOverViewReport(params?: DashboardOverviewParams) { try { const response = await API.get('/ctel/overview/', { params: { - page: params?.page, - page_size: params?.page_size, - start_date: params?.start_date, - end_date: params?.end_date, + duration: params?.duration, subsidiary: params?.subsidiary, }, }); @@ -88,17 +86,69 @@ export async function getOverViewReport(params?: ReportListParams) { } } -export async function downloadReport(report_id: string) { +export async function downloadReport(report_id: string, downloadFinishedCallback?: (fileDetails: any) => void) { try { const response = await API.get(`/ctel/get_report_file/${report_id}/`, { responseType: 'blob', // Important }); + let filename = "report.xlsx"; + try { + let basename = response.headers['content-disposition'].split('filename=')[1].split('.')[0]; + if (basename[0] == '_') { + basename = basename.substring(1); + } + filename = `${basename}.xlsx` + } catch(err) { + console.log(err); + } + const file = new Blob([response.data], { + type: 'application/vnd.ms-excel', + }); + downloadFinishedCallback && downloadFinishedCallback({ + file: file, + filename: filename, + }); + return { + file: file, + filename: filename, + } + } catch (error) { + downloadFinishedCallback && downloadFinishedCallback({ + file: null, + filename: null, + }); + notification.error({ + message: `${error?.message}`, + }); + console.log(error); + } +} + + +export async function downloadDashboardReport(duration='30d', subsidiary='ALL') { + try { + const response = await API.get(`/ctel/overview_download_file/?duration=${duration}&subsidiary=${subsidiary}`, { + responseType: 'blob', // Important + }); + let filename = "report.xlsx"; + try { + let basename = response.headers['content-disposition'].split('filename=')[1].split('.')[0]; + if (basename[0] == '_') { + basename = basename.substring(1); + } + filename = `${basename}.xlsx` + } catch(err) { + console.log(err); + } const file = new Blob([response.data], { type: 'application/vnd.ms-excel', }); // const fileURL = URL.createObjectURL(file); // window.open(fileURL); - return file; + return { + file: file, + filename: filename, + } } catch (error) { notification.error({ message: `${error?.message}`, diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 44f8c94..1d9e946 100755 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -84,12 +84,12 @@ services: depends_on: db-sbt: condition: service_started - # command: sh -c "chmod -R 777 /app; sleep 5; python manage.py collectstatic --no-input && - # python manage.py makemigrations && - # python manage.py migrate && - # python manage.py compilemessages && - # gunicorn fwd.asgi:application -k uvicorn.workers.UvicornWorker --timeout 300 -b 0.0.0.0:9000" # pre-makemigrations on prod - command: bash -c "tail -f > /dev/null" + command: sh -c "chmod -R 777 /app; sleep 5; python manage.py collectstatic --no-input && + python manage.py makemigrations && + python manage.py migrate && + python manage.py compilemessages && + gunicorn fwd.asgi:application -k uvicorn.workers.UvicornWorker --timeout 300 -b 0.0.0.0:9000" # pre-makemigrations on prod + # command: bash -c "tail -f > /dev/null" minio: image: minio/minio @@ -175,6 +175,7 @@ services: working_dir: /app command: sh -c "celery -A fwd_api.celery_worker.worker worker -l INFO -c 5" + # command: bash -c "tail -f > /dev/null" # Back-end persistent db-sbt: diff --git a/document-classification-kv-demo b/document-classification-kv-demo new file mode 160000 index 0000000..220954c --- /dev/null +++ b/document-classification-kv-demo @@ -0,0 +1 @@ +Subproject commit 220954c5c6bfed15e93e26b2adacf28ff8b75baf diff --git a/junk_tests/date_compare.py b/junk_tests/date_compare.py new file mode 100644 index 0000000..1e5e4f4 --- /dev/null +++ b/junk_tests/date_compare.py @@ -0,0 +1,17 @@ +from datetime import datetime + +# Assuming you have two datetime objects for the same day in different months +date_jan = datetime(2022, 2, 15, 12, 30, 0) +date_feb = datetime(2022, 2, 15, 8, 45, 0) + +# Check if they are the same day +if date_jan.day == date_feb.day and date_jan.month == date_feb.month and date_jan.year == date_feb.year: + print("They are the same day") +else: + print("They are different days") + +# Check if they are the same month +if date_jan.month == date_feb.month and date_jan.year == date_feb.year: + print("They are the same month") +else: + print("They are different months") \ No newline at end of file