diff --git a/Modules/Internal/Http/Controllers/Api/AuditTrailController.php b/Modules/Internal/Http/Controllers/Api/AuditTrailController.php new file mode 100644 index 00000000..dd5ca4e4 --- /dev/null +++ b/Modules/Internal/Http/Controllers/Api/AuditTrailController.php @@ -0,0 +1,90 @@ +where('model', '=', $request->model) + ->where('model_id', '=', $id) + // ->latest() + ->paginate(1000); + return response()->json(Helper::paginateResources(AuditTrailResource::collection($audittrails))); + } + /** + * Show the form for creating a new resource. + * @return Renderable + */ + public function create() + { + return view('internal::create'); + } + + /** + * Store a newly created resource in storage. + * @param Request $request + * @return Renderable + */ + public function store(Request $request) + { + // + } + + /** + * Show the specified resource. + * @param int $id + * @return Renderable + */ + public function show($id) + { + $appointments = Appointment::query() + ->with('doctor.user', 'doctor.speciality', 'appointmentDetail', 'healthCare') + ->where('nID', $id) + ->first(); + return response()->json(new AppointmentResource($appointments)); + } + + /** + * Show the form for editing the specified resource. + * @param int $id + * @return Renderable + */ + public function edit($id) + { + return view('internal::edit'); + } + + /** + * Update the specified resource in storage. + * @param Request $request + * @param int $id + * @return Renderable + */ + public function update(Request $request, $id) + { + // + } + + /** + * Remove the specified resource from storage. + * @param int $id + * @return Renderable + */ + public function destroy($id) + { + // + } +} +?> \ No newline at end of file diff --git a/Modules/Internal/Http/Controllers/Api/CorporateController.php b/Modules/Internal/Http/Controllers/Api/CorporateController.php index fbc4d638..5aa3e3d3 100755 --- a/Modules/Internal/Http/Controllers/Api/CorporateController.php +++ b/Modules/Internal/Http/Controllers/Api/CorporateController.php @@ -684,5 +684,26 @@ class CorporateController extends Controller "file_url" => url('files/CorporatePlan&BenefitImportData.xlsx') ]); } + + public function corporateCode(Request $request){ + $corporates = Corporate::query() + ->when($request->search, function ($query, $search) { + return $query->where('name', 'LIKE', '%' . $search . '%') + ->orWhere('code', 'LIKE', '%' . $search . '%'); + }) + ->with('currentPolicy', 'subCorporates') + ->withCount([ + 'employees', + 'corporateBenefits', + 'corporatePlans', + + // 'claims' + ]) + ->where('active', 1) + ->get() + ->pluck('code'); + + return $corporates; + } } diff --git a/Modules/Internal/Http/Controllers/Api/CorporatePlanController.php b/Modules/Internal/Http/Controllers/Api/CorporatePlanController.php index 23e016a7..bcf8c232 100755 --- a/Modules/Internal/Http/Controllers/Api/CorporatePlanController.php +++ b/Modules/Internal/Http/Controllers/Api/CorporatePlanController.php @@ -35,6 +35,7 @@ class CorporatePlanController extends Controller $plan = CorporatePlan::findOrFail($plan_id); $plan->active = $request->active == '1'; + $plan->reason = $request->reason; if ($plan->save()) { return response()->json([ diff --git a/Modules/Internal/Http/Controllers/Api/CorporateServiceController.php b/Modules/Internal/Http/Controllers/Api/CorporateServiceController.php index b40727a6..87892a76 100755 --- a/Modules/Internal/Http/Controllers/Api/CorporateServiceController.php +++ b/Modules/Internal/Http/Controllers/Api/CorporateServiceController.php @@ -136,6 +136,7 @@ class CorporateServiceController extends Controller public function corporateServiceUpdate(Request $request, $corporate_id, $service_code) { + // dd($request->all); // $corporate = Corporate::findOrFail($corporate_id); $corporateService = CorporateService::query() ->where('corporate_id', $corporate_id) @@ -143,7 +144,8 @@ class CorporateServiceController extends Controller // ->with('configs', 'service') ->first(); $corporateService->fill([ - 'status' => $request->status == 'active' ? 'active' : 'inactive' + 'status' => $request->status == 'active' ? 'active' : 'inactive', + 'reason' => $request->reason ]); $corporateService->save(); diff --git a/Modules/Internal/Routes/api.php b/Modules/Internal/Routes/api.php index 40b65fa6..ac4f01d9 100755 --- a/Modules/Internal/Routes/api.php +++ b/Modules/Internal/Routes/api.php @@ -30,6 +30,7 @@ use Modules\Internal\Http\Controllers\Api\PlanController; use Modules\Internal\Http\Controllers\Api\ProvinceController; use Modules\Internal\Http\Controllers\Api\SpecialityController; use Modules\Internal\Http\Controllers\Api\VillageController; +use Modules\Internal\Http\Controllers\Api\AuditTrailController; use Modules\Internal\Http\Controllers\ClaimEncounterController; /* @@ -67,6 +68,7 @@ Route::prefix('internal')->group(function () { Route::put('corporates/{corporate_id}/activation', [CorporateController::class, 'activation']); Route::post('corporates/{corporate_id}/import-plan-benefit', [CorporateController::class, 'importPlanBenefit']); Route::get('corporates/{corporate_id}/data-plan-benefit', [CorporateController::class, 'dataPlanBenefit']); + Route::get('corporates/{corporate_id}/code', [CorporateController::class, 'corporateCode']); Route::get('corporates/{corporate_id}/corporate-plans', [CorporatePlanController::class, 'index']); Route::post('corporates/{corporate_id}/corporate-plans', [CorporatePlanController::class, 'store']); @@ -115,6 +117,9 @@ Route::prefix('internal')->group(function () { // Route::get('corporates/{corporate_id}/diagnosis-exclusions', [DiagnosisExclusionController::class, 'index']); // Route::get('corporates/{corporate_id}/diagnosis-exclusions/import', [DiagnosisExclusionController::class, 'import']); + // Audittrail + Route::get('audittrail/{corporate_id}', [AuditTrailController::class, 'index']); + Route::get('master/diagnosis', [DiagnosisController::class, 'index']); Route::get('master/diagnosis/search', [DiagnosisController::class, 'search']); Route::get('master/drugs', [DrugController::class, 'index']); diff --git a/Modules/Internal/Services/CorporateService.php b/Modules/Internal/Services/CorporateService.php index 3f50a787..ec5f18f8 100755 --- a/Modules/Internal/Services/CorporateService.php +++ b/Modules/Internal/Services/CorporateService.php @@ -70,10 +70,10 @@ class CorporateService 'attribute' => 'Plan' ]), 0, null, $row); } else if(!in_array($row['corporate_plan_id'], $current_corporate_plans)){ - throw new ImportRowException(__('plan.NOT_MATCH', [ - 'attribute' => 'Plans', - 'code' => $row['corporate_plan_id'] - ]), 0, null, $row); + // throw new ImportRowException(__('plan.NOT_MATCH', [ + // 'attribute' => 'Plans', + // 'code' => $row['corporate_plan_id'] + // ]), 0, null, $row); } if (empty($row['code'])) { @@ -175,10 +175,10 @@ class CorporateService if (empty($row['plan_code'])) { throw new ImportRowException(__('benefit.PLAN_CODE_REQUIRED'), 0, null, $row); } else if (!in_array($row['plan_code'], $current_corporate_plans)){ - throw new ImportRowException(__('plan.NOT_MATCH', [ - 'attribute' => 'Plan', - 'code' => $row['plan_code'] - ]), 0, null, $row); + // throw new ImportRowException(__('plan.NOT_MATCH', [ + // 'attribute' => 'Plan', + // 'code' => $row['plan_code'] + // ]), 0, null, $row); } if (empty($row['code'])) { diff --git a/Modules/Internal/Services/MemberEnrollmentService.php b/Modules/Internal/Services/MemberEnrollmentService.php index e786a790..c78d516a 100755 --- a/Modules/Internal/Services/MemberEnrollmentService.php +++ b/Modules/Internal/Services/MemberEnrollmentService.php @@ -736,7 +736,7 @@ class MemberEnrollmentService $person = Person::create([ 'name' => $row['name'], - 'birth_date' => $this->dateParser($row['date_of_birth']), + 'birth_date' => $row['date_of_birth'], 'gender' => Helper::genderPerson($row['sex']), 'language' => $row['language'] ?? null, 'race' => $row['race'] ?? null, diff --git a/Modules/Internal/Transformers/AuditTrailResource.php b/Modules/Internal/Transformers/AuditTrailResource.php new file mode 100644 index 00000000..c526e879 --- /dev/null +++ b/Modules/Internal/Transformers/AuditTrailResource.php @@ -0,0 +1,30 @@ + $this->id, + 'old_values' => json_decode($this->old_values), + 'new_values' => json_decode($this->new_values), + 'action' => $this->action, + 'user_id' => $this->user->email, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + + return $audit_trail; + } +} diff --git a/app/Models/AuditTrail.php b/app/Models/AuditTrail.php new file mode 100644 index 00000000..682c2e42 --- /dev/null +++ b/app/Models/AuditTrail.php @@ -0,0 +1,29 @@ + 'json', + 'new_values' => 'json', + ]; + + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/Corporate.php b/app/Models/Corporate.php index c1c1817f..f6b922a7 100755 --- a/app/Models/Corporate.php +++ b/app/Models/Corporate.php @@ -1,7 +1,7 @@ trim(collect(explode($sep, $value))->map(function ($segment) { return $segment[0] ?? ''; })->join($glue))); + + // Menambahkan event listener untuk jejak audit pada model yang ingin di-audit + // Corporate::created(function ($model) { + // $this->logAuditTrail($model, 'created'); + // }); + + Corporate::updated(function ($model) { + + $this->logAuditTrail($model, 'updated'); + }); + + Corporate::deleted(function ($model) { + $this->logAuditTrail($model, 'deleted'); + }); + + + // Corporate Service + CorporateService::updated(function ($model) { + + $this->logAuditTrail($model, 'updated'); + }); + + CorporateService::deleted(function ($model) { + $this->logAuditTrail($model, 'deleted'); + }); + + // Corporate Plans + CorporatePlan::updated(function ($model) { + + $this->logAuditTrail($model, 'updated'); + }); + + CorporatePlan::deleted(function ($model) { + $this->logAuditTrail($model, 'deleted'); + }); + + + } + + private function logAuditTrail($model, $action) + { + // Membuat jejak audit baru + $auditTrail = new AuditTrail([ + 'model' => get_class($model), + 'model_id' => $model->getKey(), + 'action' => $action, + 'old_values' => json_encode($model->getOriginal()), + 'new_values' => json_encode($model->getAttributes()), + 'user_id' => Auth::id(), + ]); + + // Simpan jejak audit + $auditTrail->save(); } } diff --git a/database/migrations/2023_05_29_124603_create_reason_update_data.php b/database/migrations/2023_05_29_124603_create_reason_update_data.php new file mode 100644 index 00000000..1a48545d --- /dev/null +++ b/database/migrations/2023_05_29_124603_create_reason_update_data.php @@ -0,0 +1,35 @@ +id(); + $table->integer('menu_id'); + $table->integer('data_id'); + $table->string('reason'); + $table->string('edited_by'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('reason_update_data'); + } +}; diff --git a/database/migrations/2023_05_29_140058_create_audit_trails_table.php b/database/migrations/2023_05_29_140058_create_audit_trails_table.php new file mode 100644 index 00000000..c68edc1d --- /dev/null +++ b/database/migrations/2023_05_29_140058_create_audit_trails_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('model'); + $table->unsignedBigInteger('model_id'); + $table->string('action'); + $table->text('old_values')->nullable(); + $table->text('new_values')->nullable(); + $table->unsignedBigInteger('user_id')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('audit_trails'); + } +}; diff --git a/database/migrations/2023_05_30_112637_add_column_reason_to_corporates.php b/database/migrations/2023_05_30_112637_add_column_reason_to_corporates.php new file mode 100644 index 00000000..323d6523 --- /dev/null +++ b/database/migrations/2023_05_30_112637_add_column_reason_to_corporates.php @@ -0,0 +1,32 @@ +string('reason')->after('name')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('corporates', function (Blueprint $table) { + $table->dropColumn('reason'); + }); + } +}; diff --git a/database/migrations/2023_05_31_153700_add_reason_to_corporate_services_table.php b/database/migrations/2023_05_31_153700_add_reason_to_corporate_services_table.php new file mode 100644 index 00000000..a6b5407a --- /dev/null +++ b/database/migrations/2023_05_31_153700_add_reason_to_corporate_services_table.php @@ -0,0 +1,34 @@ +string('reason')->after('service_code')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('corporate_services', function (Blueprint $table) { + // + $table->dropColumn('reason'); + }); + } +}; diff --git a/database/migrations/2023_06_02_145626_add_reason_to_plans_table.php b/database/migrations/2023_06_02_145626_add_reason_to_plans_table.php new file mode 100644 index 00000000..42417de8 --- /dev/null +++ b/database/migrations/2023_06_02_145626_add_reason_to_plans_table.php @@ -0,0 +1,33 @@ +string('reason')->after('service_code')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('plans', function (Blueprint $table) { + // + $table->dropColumn('reason'); + }); + } +}; diff --git a/frontend/dashboard/src/@types/corporates.ts b/frontend/dashboard/src/@types/corporates.ts index 03c941d7..45ad95ca 100755 --- a/frontend/dashboard/src/@types/corporates.ts +++ b/frontend/dashboard/src/@types/corporates.ts @@ -178,6 +178,7 @@ export type CorporateService = { description?: string; name?: string; service_code: string; + reason: string; status: string; configurations: any; } diff --git a/frontend/dashboard/src/pages/Corporates/Benefit/List.tsx b/frontend/dashboard/src/pages/Corporates/Benefit/List.tsx index cf4d38d5..0989a5cd 100755 --- a/frontend/dashboard/src/pages/Corporates/Benefit/List.tsx +++ b/frontend/dashboard/src/pages/Corporates/Benefit/List.tsx @@ -358,7 +358,7 @@ export default function PlanList() { )} - {/* + - */} + {/* COLLAPSIBLE ROW */} @@ -799,9 +799,9 @@ export default function PlanList() { Status - {/* + Action - */} + {dataTableIsLoading ? ( diff --git a/frontend/dashboard/src/pages/Corporates/Benefit/sections/DialogLog.tsx b/frontend/dashboard/src/pages/Corporates/Benefit/sections/DialogLog.tsx new file mode 100755 index 00000000..1b5c4b25 --- /dev/null +++ b/frontend/dashboard/src/pages/Corporates/Benefit/sections/DialogLog.tsx @@ -0,0 +1,213 @@ +import * as Yup from 'yup'; +import { enqueueSnackbar, useSnackbar } from 'notistack'; +import { useNavigate } from 'react-router-dom'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +// @mui +import { styled } from '@mui/material/styles'; +import { LoadingButton } from '@mui/lab'; +import { Box, Button, Grid, Stack, Typography, Chip, Autocomplete } from '@mui/material'; +import { CorporateService } from '../../../../@types/corporates'; +// components +import { FormProvider, RHFTextField, RHFSwitch } from '../../../../components/hook-form'; +import axios from '../../../../utils/axios'; +import { LaravelPaginatedData } from '../../../../@types/paginated-data'; + +// import { Contact } from '../../../../@types/contact'; +import { Link, useParams, useSearchParams } from 'react-router-dom'; + +// @mui +// components +import MuiDialog from '../../../../components/MuiDialog'; +// React +import { ReactElement } from 'react'; + +// ---------------------------------------------------------------------- + +const HeaderStyle = styled('header')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + padding: theme.spacing(2), + justifyContent: 'space-between', +})); +type DataContent = { + info: string; + date: string; + time: string; +}; + +type MuiDialogProps = { + title?: { + name?: string; + icon?: string; + }; + openDialog: boolean; + setOpenDialog: Function; + content?: ReactElement; + data?: DataContent[]; +}; + +type FormValuesProps = { + value: string; + active: boolean; +}; + +// ---------------------------------------------------------------------- + +// ---------------------------------------------------------------------- + +const DialogTopUpLimit = ({ title, openDialog, setOpenDialog, data }: MuiDialogProps) => { + const navigate = useNavigate(); + const [dataTableData, setDataTableData] = useState({ + current_page: 1, + data: [], + path: '', + first_page_url: '', + last_page: 1, + last_page_url: '', + next_page_url: '', + prev_page_url: '', + per_page: 10, + from: 0, + to: 0, + total: 0, + }); + + const { id, service_code, status } = data; + + const isEdit = id ? true : false; + + const NewCorporateSchema = Yup.object().shape({ + reason: Yup.string().required('Corporate Status is required'), + }); + + + const methods = useForm({ + resolver: yupResolver(NewCorporateSchema), + }); + const { + reset, + watch, + handleSubmit, + formState: { isSubmitting }, + } = methods; + + const values = watch(); + useEffect(() => { + if (openDialog === false) { + reset(); + } + }, [openDialog, reset]); + + // const { plan_id } = useParams(); + const handleActivate = (model: any, status: string) => { + axios + .put(`/plans/${id}/activation`, { + // service_code: service.service_code, + active: status == 'active', + reason: model.reason + }) + .then((res) => { + // Memuat ulang halaman saat ini + setOpenDialog(false) + window.location.reload(); + // setDataTableData({ + // ...dataTableData, + // data: dataTableData.data.map((service) => { + // let updatedService = service; + // if (id == service.id) { + // updatedService.status = res.data.status; + // } + // return updatedService; + // }), + // }); + }) + .catch((error) => { + console.log(error); + }); + }; + + const onSubmit = async (row : ReturnType) => { + try { + const data = { + service_code : service_code, + reason : row.reason, + id : id, + } + console.log(data) + handleActivate(data, status) + } catch (error: any) { + console.log('data gagal', data); + } + + const ascent = document?.querySelector('ascent'); + if (ascent != null) { + ascent.innerHTML = ''; + } + }; + + function createData(corporateService: CorporateService): CorporateService { + return { + ...corporateService, + }; + } + + const getContent = (props: { row: ReturnType }) => ( + + + + + + + + + + + + + Save + + + + + + + + ); + + return ( + + ); +}; + +export default DialogTopUpLimit; \ No newline at end of file diff --git a/frontend/dashboard/src/pages/Corporates/Benefit/sections/History.tsx b/frontend/dashboard/src/pages/Corporates/Benefit/sections/History.tsx new file mode 100644 index 00000000..c1f372cf --- /dev/null +++ b/frontend/dashboard/src/pages/Corporates/Benefit/sections/History.tsx @@ -0,0 +1,177 @@ +// @mui +import { + Box, + Button, + Card, + Collapse, + Container, + FormControl, + Grid, + IconButton, + InputLabel, + MenuItem, + OutlinedInput, + Paper, + Select, + SelectChangeEvent, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, + Badge, + Stack, +} from '@mui/material'; +import * as React from 'react'; +import { useParams } from 'react-router-dom'; +import { styled } from '@mui/material/styles'; +import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp'; +import MuiAccordion, { AccordionProps } from '@mui/material/Accordion'; +import { useContext, useEffect, useState } from 'react'; +import MuiAccordionSummary, { + AccordionSummaryProps, +} from '@mui/material/AccordionSummary'; +import useSettings from '../../../../hooks/useSettings'; +import axios from '../../../../utils/axios'; +import { ConfiguredCorporateContext } from '@/contexts/ConfiguredCorporateContext'; +import MuiAccordionDetails from '@mui/material/AccordionDetails'; +import HeaderBreadcrumbs from '../../../../components/HeaderBreadcrumbs'; +import { Corporate } from '@/@types/corporates'; +import { fDate, fDateTime } from '@/utils/formatTime'; + +const Accordion = styled((props: AccordionProps) => ( + +))(({ theme }) => ({ + border: `1px solid ${theme.palette.divider}`, + '&:not(:last-child)': { + borderBottom: 0, + }, + '&:before': { + display: 'none', + }, +})); + +const AccordionSummary = styled((props: AccordionSummaryProps) => ( + } + {...props} + /> +))(({ theme }) => ({ + backgroundColor: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, .05)' + : 'rgba(0, 0, 0, .03)', + flexDirection: 'row-reverse', + '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { + transform: 'rotate(90deg)', + }, + '& .MuiAccordionSummary-content': { + marginLeft: theme.spacing(1), + }, +})); + +const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ + padding: theme.spacing(2), + borderTop: '1px solid rgba(0, 0, 0, .125)', +})); + +export default function CustomizedAccordions() { + const [expanded, setExpanded] = React.useState('panel1'); + + const handleChange = + (panel: string) => (event: React.SyntheticEvent, newExpanded: boolean) => { + setExpanded(newExpanded ? panel : false); + }; + const pageTitle = 'Audittrail Corporate'; + + const { themeStretch } = useSettings(); + + const { corporate_id, plan_id } = useParams(); + + const [corporate, setCorporate] = useState(); + const [currentCorporate, setCurrentCorporate ] = useState(); + + const configuredCorporateContext = useContext(ConfiguredCorporateContext); + + useEffect(() => { + setCorporate(configuredCorporateContext.currentCorporate); + const model = 'App\\Models\\CorporatePlan'; + const url = `/audittrail/${plan_id}?model=${model}`; + axios.get(url) + .then((res) => { + setCurrentCorporate(res.data); + }) + .catch((error) => { + console.error('Terjadi kesalahan:', error); + }); + + }, [configuredCorporateContext]); + + return ( +
+ + {currentCorporate?.data.map((item, index) => ( + + + {`Data has ${item.action} by ${item.user_id} on ${fDateTime(item.updated_at)}`} + + + + + Field + Old Value + New Values + + + + {Object.entries(item.old_values).map(([key, value]) => { + let renderedValue; + if (key !== 'reason') { + return null; // Melewati iterasi saat key adalah 'deleted_by' + } + renderedValue = item.new_values[key]; + + const field = key.charAt(0).toUpperCase() + key.slice(1); + + return ( + + {`${field}`} + {`${value}`} + {renderedValue} + + ); + })} + + + + + ))} +
+ ); +} diff --git a/frontend/dashboard/src/pages/Corporates/Form.tsx b/frontend/dashboard/src/pages/Corporates/Form.tsx index 9a7eab2c..a1a8b935 100755 --- a/frontend/dashboard/src/pages/Corporates/Form.tsx +++ b/frontend/dashboard/src/pages/Corporates/Form.tsx @@ -62,59 +62,115 @@ type Props = { }; export default function CorporateForm({ isEdit, currentCorporate }: Props) { + const navigate = useNavigate(); const [corporate_groups, setCorporateGroups] = useState([]); // const [ errors, setErrors ] = useState<{ [key: string]: string }>({}); const { enqueueSnackbar } = useSnackbar(); - - const NewCorporateSchema = Yup.object().shape({ - name: Yup.string().required('Name is required'), - code: Yup.string().required('Corporate Code is required').test( - 'unique-code', - 'Code must be unique', - async function (value) { - const existingCodes = await getExistingCodes(); - return !existingCodes.includes(value); - } - ), - active: Yup.boolean().required('Corporate Status is required'), - type: Yup.string().required('Type is required'), - welcome_message: Yup.string().required('Welcome Message is required'), - help_text: Yup.string().required('Help Text is required'), - // policy_code: Yup.string().required('Policy Code is required'), - policy_start: Yup.date().required('Start Date is required'), - policy_end: Yup.date().required('End Date is required').min(Yup.ref('policy_start'), "end date can't be before start date"), - policy_total_premi: Yup.number().required('Deposit Initial Fund is required').min(0), - // linking_rules: Yup.string().required('Link Rules is required'), - policy_minimal_deposit_percentage: - Yup.number() - .typeError("Please enter a valid number") - .required('Percentage Deposit is required') - .min(0, "Minimum atleast 0") - .max(100, "Allowed maximum is 100"), - policy_minimal_alert_percentage: - Yup.number() - .typeError("Please enter a valid number") - .required('Percentage Alert is required') - .min(0, "Minimum atleast 0") - .max(100, "Allowed maximum is 100"), - policy_stop_service_percentage: - Yup.number() - .typeError("Please enter a valid number") - .min(0, "Minimum atleast 0") - .required('Percentage Stop is required') - .test("max", "Total should not exceed 100 %", function(value) { - const { policy_minimal_alert_percentage } = this.parent; - const { policy_minimal_deposit_percentage } = this.parent; - return value == 100 - policy_minimal_alert_percentage- policy_minimal_deposit_percentage; - }), - parent_id: Yup.string().when('type', { - is: 'subcorporate', - then: Yup.string().required('Corporate is required because type is Sub Corporate'), - }), - }); + let NewCorporateSchema = null; + if (isEdit){ + NewCorporateSchema = Yup.object().shape({ + isEdited: Yup.boolean(), + name: Yup.string().required('Name is required'), + code: Yup.string().required('Corporate Code is required').test( + 'unique-code', + 'Code must be unique', + async function (value) { + const existingCodes = await getExistingCodes(); + return !existingCodes.includes(value); + } + ), + active: Yup.boolean().required('Corporate Status is required'), + type: Yup.string().required('Type is required'), + welcome_message: Yup.string().required('Welcome Message is required'), + help_text: Yup.string().required('Help Text is required'), + // policy_code: Yup.string().required('Policy Code is required'), + policy_start: Yup.date().required('Start Date is required'), + policy_end: Yup.date().required('End Date is required').min(Yup.ref('policy_start'), "end date can't be before start date"), + policy_total_premi: Yup.number().required('Deposit Initial Fund is required').min(0), + // linking_rules: Yup.string().required('Link Rules is required'), + policy_minimal_deposit_percentage: + Yup.number() + .typeError("Please enter a valid number") + .required('Percentage Deposit is required') + .min(0, "Minimum atleast 0") + .max(100, "Allowed maximum is 100"), + policy_minimal_alert_percentage: + Yup.number() + .typeError("Please enter a valid number") + .required('Percentage Alert is required') + .min(0, "Minimum atleast 0") + .max(100, "Allowed maximum is 100"), + policy_stop_service_percentage: + Yup.number() + .typeError("Please enter a valid number") + .min(0, "Minimum atleast 0") + .required('Percentage Stop is required') + .test("max", "Total should not exceed 100 %", function(value) { + const { policy_minimal_alert_percentage } = this.parent; + const { policy_minimal_deposit_percentage } = this.parent; + return value == 100 - policy_minimal_alert_percentage- policy_minimal_deposit_percentage; + }), + parent_id: Yup.string().when('type', { + is: 'subcorporate', + then: Yup.string().required('Corporate is required because type is Sub Corporate'), + }), + + reason: Yup.string().required('Reason for update is required when editing data'), + }); + } else { + // console.log('test') + NewCorporateSchema = Yup.object().shape({ + isEdited: Yup.boolean(), + name: Yup.string().required('Name is required'), + code: Yup.string().required('Corporate Code is required').test( + 'unique-code', + 'Code must be unique', + async function (value) { + const existingCodes = await getExistingCodes(); + return !existingCodes.includes(value); + } + ), + active: Yup.boolean().required('Corporate Status is required'), + type: Yup.string().required('Type is required'), + welcome_message: Yup.string().required('Welcome Message is required'), + help_text: Yup.string().required('Help Text is required'), + // policy_code: Yup.string().required('Policy Code is required'), + policy_start: Yup.date().required('Start Date is required'), + policy_end: Yup.date().required('End Date is required').min(Yup.ref('policy_start'), "end date can't be before start date"), + policy_total_premi: Yup.number().required('Deposit Initial Fund is required').min(0), + // linking_rules: Yup.string().required('Link Rules is required'), + policy_minimal_deposit_percentage: + Yup.number() + .typeError("Please enter a valid number") + .required('Percentage Deposit is required') + .min(0, "Minimum atleast 0") + .max(100, "Allowed maximum is 100"), + policy_minimal_alert_percentage: + Yup.number() + .typeError("Please enter a valid number") + .required('Percentage Alert is required') + .min(0, "Minimum atleast 0") + .max(100, "Allowed maximum is 100"), + policy_stop_service_percentage: + Yup.number() + .typeError("Please enter a valid number") + .min(0, "Minimum atleast 0") + .required('Percentage Stop is required') + .test("max", "Total should not exceed 100 %", function(value) { + const { policy_minimal_alert_percentage } = this.parent; + const { policy_minimal_deposit_percentage } = this.parent; + return value == 100 - policy_minimal_alert_percentage- policy_minimal_deposit_percentage; + }), + parent_id: Yup.string().when('type', { + is: 'subcorporate', + then: Yup.string().required('Corporate is required because type is Sub Corporate'), + }), + }); + } + async function getExistingCodes() { // axios @@ -127,15 +183,16 @@ export default function CorporateForm({ isEdit, currentCorporate }: Props) { // }); try { - let response = await axios.get('/corporates'); // get data all corporate + let response = await axios.get('/corporates/1/code'); // get data all corporate let codeCurrent = "" if (isEdit){ let responseCodeCurrent = await axios.get(`/corporates/${currentCorporate?.id}/edit`); // get data current corporate codeCurrent = responseCodeCurrent.data.code; // get data code corporate current } - let existingCodes = response.data.data.map(item => item.code); // get data code corporate all + // console.log(response.data); + let existingCodes = response.data.map(item => item); // get data code corporate all - var filteredArray = existingCodes.filter(e => e != codeCurrent) + let filteredArray = existingCodes.filter(e => e != codeCurrent) return filteredArray; } catch (error) { console.error(error); @@ -147,6 +204,7 @@ export default function CorporateForm({ isEdit, currentCorporate }: Props) { () => ({ code: currentCorporate?.code || '', name: currentCorporate?.name || '', + reason: currentCorporate?.reason || '', welcome_message: currentCorporate?.welcome_message || '', help_text: currentCorporate?.help_text || '', active: currentCorporate?.id ? currentCorporate?.active === 1 : true, @@ -224,6 +282,7 @@ export default function CorporateForm({ isEdit, currentCorporate }: Props) { formData.append('active', data.active ? '1' : '0'); formData.append('type', data.type); formData.append('welcome_message', data.welcome_message); + formData.append('reason', data.reason); formData.append('help_text', data.help_text); formData.append('policy_id', data.policy_id); formData.append('policy_code', data.policy_code); @@ -390,7 +449,7 @@ export default function CorporateForm({ isEdit, currentCorporate }: Props) { id: 'Lilili', }, ]; - + const [isDisabled, setIsDisabled] = useState(isEdit); const handleTypeChange = (event: SelectChangeEvent) => { setValue('type', event.target.value); }; @@ -431,12 +490,15 @@ export default function CorporateForm({ isEdit, currentCorporate }: Props) { ))} + )} + + + + + {isEdit && ( + )} - - - - Welcome Message @@ -512,7 +574,7 @@ export default function CorporateForm({ isEdit, currentCorporate }: Props) { - + {!currentCorporate?.id && ( Will be generated if empty )} diff --git a/frontend/dashboard/src/pages/Corporates/History.tsx b/frontend/dashboard/src/pages/Corporates/History.tsx new file mode 100644 index 00000000..f657b5ab --- /dev/null +++ b/frontend/dashboard/src/pages/Corporates/History.tsx @@ -0,0 +1,209 @@ +// @mui +import { + Box, + Button, + Card, + Collapse, + Container, + FormControl, + Grid, + IconButton, + InputLabel, + MenuItem, + OutlinedInput, + Paper, + Select, + SelectChangeEvent, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, + Badge, + Stack, +} from '@mui/material'; +import * as React from 'react'; +import { useParams } from 'react-router-dom'; +import { styled } from '@mui/material/styles'; +import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp'; +import MuiAccordion, { AccordionProps } from '@mui/material/Accordion'; +import { useContext, useEffect, useState } from 'react'; +import MuiAccordionSummary, { + AccordionSummaryProps, +} from '@mui/material/AccordionSummary'; +import useSettings from '../../hooks/useSettings'; +import axios from '../../utils/axios'; +import { ConfiguredCorporateContext } from '@/contexts/ConfiguredCorporateContext'; +import MuiAccordionDetails from '@mui/material/AccordionDetails'; +import HeaderBreadcrumbs from '../../components/HeaderBreadcrumbs'; +import { Corporate } from '@/@types/corporates'; +import { fDate, fDateTime } from '@/utils/formatTime'; + +const Accordion = styled((props: AccordionProps) => ( + +))(({ theme }) => ({ + border: `1px solid ${theme.palette.divider}`, + '&:not(:last-child)': { + borderBottom: 0, + }, + '&:before': { + display: 'none', + }, +})); + +const AccordionSummary = styled((props: AccordionSummaryProps) => ( + } + {...props} + /> +))(({ theme }) => ({ + backgroundColor: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, .05)' + : 'rgba(0, 0, 0, .03)', + flexDirection: 'row-reverse', + '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { + transform: 'rotate(90deg)', + }, + '& .MuiAccordionSummary-content': { + marginLeft: theme.spacing(1), + }, +})); + +const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ + padding: theme.spacing(2), + borderTop: '1px solid rgba(0, 0, 0, .125)', +})); + +export default function CustomizedAccordions() { + const [expanded, setExpanded] = React.useState('panel1'); + + const handleChange = + (panel: string) => (event: React.SyntheticEvent, newExpanded: boolean) => { + setExpanded(newExpanded ? panel : false); + }; + const pageTitle = 'Audittrail Corporate'; + + const { themeStretch } = useSettings(); + + const { corporate_id } = useParams(); + + const [corporate, setCorporate] = useState(); + const [ currentCorporate, setCurrentCorporate ] = useState(); + + const configuredCorporateContext = useContext(ConfiguredCorporateContext); + + useEffect(() => { + setCorporate(configuredCorporateContext.currentCorporate); + const model = 'App\\Models\\Corporate'; + const url = `/audittrail/${corporate_id}?model=${model}`; + axios.get(url) + .then((res) => { + setCurrentCorporate(res.data); + }) + .catch((error) => { + console.error('Terjadi kesalahan:', error); + }); + + }, [configuredCorporateContext]); + + return ( +
+ + {currentCorporate?.data.map((item, index) => ( + + + {`Data has ${item.action} by ${item.user_id} on ${fDateTime(item.updated_at)}`} + + + + + Field + Old Value + New Values + + + + {Object.entries(item.old_values).map(([key, value]) => { + let renderedValue; + if (key === 'deleted_by' || key === 'created_by' || key === 'updated_by') { + return null; // Melewati iterasi saat key adalah 'deleted_by' + } + switch (key) { + case 'welcome_message': + renderedValue = item.new_values[key].replace(/<[^>]*>/g, ''); + value = value.replace(/<[^>]*>/g, ''); + break; + case 'help_text': + renderedValue = item.new_values[key].replace(/<[^>]*>/g, ''); + value = value.replace(/<[^>]*>/g, ''); + break; + case 'active': + renderedValue = item.new_values[key] == 1 ? 'Active' : 'Inactive'; + value = value == 1 ? 'Active' : 'Inactive'; + break; + case 'created_at': + renderedValue = fDateTime(item.new_values[key]); + value = fDateTime(value); + break; + case 'updated_at': + renderedValue = fDateTime(item.new_values[key]); + value = fDateTime(value); + break; + case 'updated_at': + renderedValue = fDateTime(item.new_values[key]); + value = fDateTime(value); + break; + case 'delete_at': + renderedValue = fDateTime(item.new_values[key]); + value = fDateTime(value); + break; + default: + renderedValue = item.new_values[key]; + break; + } + + const field = key.charAt(0).toUpperCase() + key.slice(1); + + return ( + + {`${field}`} + {`${value}`} + {renderedValue} + + ); + })} + + + + + ))} +
+ ); +} diff --git a/frontend/dashboard/src/pages/Corporates/Index.tsx b/frontend/dashboard/src/pages/Corporates/Index.tsx index 1d16264a..32fee6a8 100755 --- a/frontend/dashboard/src/pages/Corporates/Index.tsx +++ b/frontend/dashboard/src/pages/Corporates/Index.tsx @@ -29,6 +29,8 @@ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; import PublishIcon from '@mui/icons-material/Publish'; import AddIcon from '@mui/icons-material/Add'; +import HistoryIcon from '@mui/icons-material/History'; + // hooks import useSettings from '../../hooks/useSettings'; // components @@ -328,16 +330,21 @@ export default function Corporates() { - + - + + + + diff --git a/frontend/dashboard/src/pages/Corporates/Plan/List.tsx b/frontend/dashboard/src/pages/Corporates/Plan/List.tsx index 477c5899..01ad5264 100755 --- a/frontend/dashboard/src/pages/Corporates/Plan/List.tsx +++ b/frontend/dashboard/src/pages/Corporates/Plan/List.tsx @@ -33,10 +33,11 @@ import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; import AddIcon from '@mui/icons-material/Add'; import UploadIcon from '@mui/icons-material/Upload'; import CancelIcon from '@mui/icons-material/Cancel'; +import HistoryIcon from '@mui/icons-material/History'; // hooks import React, { ChangeEvent, Component, useEffect, useRef, useState } from 'react'; import useSettings from '../../../hooks/useSettings'; -import { useParams, useSearchParams } from 'react-router-dom'; +import { Link, useParams, useSearchParams } from 'react-router-dom'; // components import axios from '../../../utils/axios'; import { Plan } from '../../../@types/corporates'; @@ -44,6 +45,7 @@ import { LaravelPaginatedData } from '../../../@types/paginated-data'; import BasePagination from '../../../components/BasePagination'; import { enqueueSnackbar } from 'notistack'; import { LoadingButton } from '@mui/lab'; +import DialogLog from './sections/DialogLog'; export default function CorporatePlanList() { const { themeStretch } = useSettings(); @@ -51,6 +53,21 @@ export default function CorporatePlanList() { const [searchParams, setSearchParams] = useSearchParams(); const [importResult, setImportResult] = useState(null); + const [openDialog, setOpenDialog] = useState(false); + const [dialogTitle, setDialogTitle] = useState(''); + const [isDialog, setIsDialog] = useState(''); + const [edit, setEdit] = useState({}); + const clickHandler = (isDialog: string) => { + switch (isDialog) { + case 'edit': + setIsDialog(isDialog); + setOpenDialog(true); + break; + default: + break; + } + }; + function SearchInput(props: any) { // SEARCH const searchInput = useRef(null); @@ -322,7 +339,9 @@ export default function CorporatePlanList() { color="success" size="small" onClick={() => { - handleActivate(row, 'inactive'); + // handleActivate(row, 'inactive'); + clickHandler('edit'); + setEdit({id: row.id, service_code: row.service_code, status: 'inactive'}); }} > Active @@ -334,18 +353,22 @@ export default function CorporatePlanList() { color="error" size="small" onClick={() => { - handleActivate(row, 'active'); + // handleActivate(row, 'active'); + clickHandler('edit'); + setEdit({id: row.id, service_code: row.service_code, status: 'active'}); }} > Inactive )} - {/* - - */} + + + + + {/* COLLAPSIBLE ROW */} @@ -709,9 +732,9 @@ export default function CorporatePlanList() { Status - {/* + Action - */} + {dataTableIsLoading ? ( @@ -742,6 +765,15 @@ export default function CorporatePlanList() { + + {isDialog === 'edit' && ( + + )}
); } diff --git a/frontend/dashboard/src/pages/Corporates/Plan/sections/DialogLog.tsx b/frontend/dashboard/src/pages/Corporates/Plan/sections/DialogLog.tsx new file mode 100755 index 00000000..1b5c4b25 --- /dev/null +++ b/frontend/dashboard/src/pages/Corporates/Plan/sections/DialogLog.tsx @@ -0,0 +1,213 @@ +import * as Yup from 'yup'; +import { enqueueSnackbar, useSnackbar } from 'notistack'; +import { useNavigate } from 'react-router-dom'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +// @mui +import { styled } from '@mui/material/styles'; +import { LoadingButton } from '@mui/lab'; +import { Box, Button, Grid, Stack, Typography, Chip, Autocomplete } from '@mui/material'; +import { CorporateService } from '../../../../@types/corporates'; +// components +import { FormProvider, RHFTextField, RHFSwitch } from '../../../../components/hook-form'; +import axios from '../../../../utils/axios'; +import { LaravelPaginatedData } from '../../../../@types/paginated-data'; + +// import { Contact } from '../../../../@types/contact'; +import { Link, useParams, useSearchParams } from 'react-router-dom'; + +// @mui +// components +import MuiDialog from '../../../../components/MuiDialog'; +// React +import { ReactElement } from 'react'; + +// ---------------------------------------------------------------------- + +const HeaderStyle = styled('header')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + padding: theme.spacing(2), + justifyContent: 'space-between', +})); +type DataContent = { + info: string; + date: string; + time: string; +}; + +type MuiDialogProps = { + title?: { + name?: string; + icon?: string; + }; + openDialog: boolean; + setOpenDialog: Function; + content?: ReactElement; + data?: DataContent[]; +}; + +type FormValuesProps = { + value: string; + active: boolean; +}; + +// ---------------------------------------------------------------------- + +// ---------------------------------------------------------------------- + +const DialogTopUpLimit = ({ title, openDialog, setOpenDialog, data }: MuiDialogProps) => { + const navigate = useNavigate(); + const [dataTableData, setDataTableData] = useState({ + current_page: 1, + data: [], + path: '', + first_page_url: '', + last_page: 1, + last_page_url: '', + next_page_url: '', + prev_page_url: '', + per_page: 10, + from: 0, + to: 0, + total: 0, + }); + + const { id, service_code, status } = data; + + const isEdit = id ? true : false; + + const NewCorporateSchema = Yup.object().shape({ + reason: Yup.string().required('Corporate Status is required'), + }); + + + const methods = useForm({ + resolver: yupResolver(NewCorporateSchema), + }); + const { + reset, + watch, + handleSubmit, + formState: { isSubmitting }, + } = methods; + + const values = watch(); + useEffect(() => { + if (openDialog === false) { + reset(); + } + }, [openDialog, reset]); + + // const { plan_id } = useParams(); + const handleActivate = (model: any, status: string) => { + axios + .put(`/plans/${id}/activation`, { + // service_code: service.service_code, + active: status == 'active', + reason: model.reason + }) + .then((res) => { + // Memuat ulang halaman saat ini + setOpenDialog(false) + window.location.reload(); + // setDataTableData({ + // ...dataTableData, + // data: dataTableData.data.map((service) => { + // let updatedService = service; + // if (id == service.id) { + // updatedService.status = res.data.status; + // } + // return updatedService; + // }), + // }); + }) + .catch((error) => { + console.log(error); + }); + }; + + const onSubmit = async (row : ReturnType) => { + try { + const data = { + service_code : service_code, + reason : row.reason, + id : id, + } + console.log(data) + handleActivate(data, status) + } catch (error: any) { + console.log('data gagal', data); + } + + const ascent = document?.querySelector('ascent'); + if (ascent != null) { + ascent.innerHTML = ''; + } + }; + + function createData(corporateService: CorporateService): CorporateService { + return { + ...corporateService, + }; + } + + const getContent = (props: { row: ReturnType }) => ( + + + + + + + + + + + + + Save + + + + + + + + ); + + return ( + + ); +}; + +export default DialogTopUpLimit; \ No newline at end of file diff --git a/frontend/dashboard/src/pages/Corporates/Plan/sections/History.tsx b/frontend/dashboard/src/pages/Corporates/Plan/sections/History.tsx new file mode 100644 index 00000000..c1f372cf --- /dev/null +++ b/frontend/dashboard/src/pages/Corporates/Plan/sections/History.tsx @@ -0,0 +1,177 @@ +// @mui +import { + Box, + Button, + Card, + Collapse, + Container, + FormControl, + Grid, + IconButton, + InputLabel, + MenuItem, + OutlinedInput, + Paper, + Select, + SelectChangeEvent, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, + Badge, + Stack, +} from '@mui/material'; +import * as React from 'react'; +import { useParams } from 'react-router-dom'; +import { styled } from '@mui/material/styles'; +import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp'; +import MuiAccordion, { AccordionProps } from '@mui/material/Accordion'; +import { useContext, useEffect, useState } from 'react'; +import MuiAccordionSummary, { + AccordionSummaryProps, +} from '@mui/material/AccordionSummary'; +import useSettings from '../../../../hooks/useSettings'; +import axios from '../../../../utils/axios'; +import { ConfiguredCorporateContext } from '@/contexts/ConfiguredCorporateContext'; +import MuiAccordionDetails from '@mui/material/AccordionDetails'; +import HeaderBreadcrumbs from '../../../../components/HeaderBreadcrumbs'; +import { Corporate } from '@/@types/corporates'; +import { fDate, fDateTime } from '@/utils/formatTime'; + +const Accordion = styled((props: AccordionProps) => ( + +))(({ theme }) => ({ + border: `1px solid ${theme.palette.divider}`, + '&:not(:last-child)': { + borderBottom: 0, + }, + '&:before': { + display: 'none', + }, +})); + +const AccordionSummary = styled((props: AccordionSummaryProps) => ( + } + {...props} + /> +))(({ theme }) => ({ + backgroundColor: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, .05)' + : 'rgba(0, 0, 0, .03)', + flexDirection: 'row-reverse', + '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { + transform: 'rotate(90deg)', + }, + '& .MuiAccordionSummary-content': { + marginLeft: theme.spacing(1), + }, +})); + +const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ + padding: theme.spacing(2), + borderTop: '1px solid rgba(0, 0, 0, .125)', +})); + +export default function CustomizedAccordions() { + const [expanded, setExpanded] = React.useState('panel1'); + + const handleChange = + (panel: string) => (event: React.SyntheticEvent, newExpanded: boolean) => { + setExpanded(newExpanded ? panel : false); + }; + const pageTitle = 'Audittrail Corporate'; + + const { themeStretch } = useSettings(); + + const { corporate_id, plan_id } = useParams(); + + const [corporate, setCorporate] = useState(); + const [currentCorporate, setCurrentCorporate ] = useState(); + + const configuredCorporateContext = useContext(ConfiguredCorporateContext); + + useEffect(() => { + setCorporate(configuredCorporateContext.currentCorporate); + const model = 'App\\Models\\CorporatePlan'; + const url = `/audittrail/${plan_id}?model=${model}`; + axios.get(url) + .then((res) => { + setCurrentCorporate(res.data); + }) + .catch((error) => { + console.error('Terjadi kesalahan:', error); + }); + + }, [configuredCorporateContext]); + + return ( +
+ + {currentCorporate?.data.map((item, index) => ( + + + {`Data has ${item.action} by ${item.user_id} on ${fDateTime(item.updated_at)}`} + + + + + Field + Old Value + New Values + + + + {Object.entries(item.old_values).map(([key, value]) => { + let renderedValue; + if (key !== 'reason') { + return null; // Melewati iterasi saat key adalah 'deleted_by' + } + renderedValue = item.new_values[key]; + + const field = key.charAt(0).toUpperCase() + key.slice(1); + + return ( + + {`${field}`} + {`${value}`} + {renderedValue} + + ); + })} + + + + + ))} +
+ ); +} diff --git a/frontend/dashboard/src/pages/Corporates/Services/List.tsx b/frontend/dashboard/src/pages/Corporates/Services/List.tsx index c7345f6f..3530572b 100755 --- a/frontend/dashboard/src/pages/Corporates/Services/List.tsx +++ b/frontend/dashboard/src/pages/Corporates/Services/List.tsx @@ -35,6 +35,7 @@ import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; import AddIcon from '@mui/icons-material/Add'; import UploadIcon from '@mui/icons-material/Upload'; import CancelIcon from '@mui/icons-material/Cancel'; +import HistoryIcon from '@mui/icons-material/History'; // hooks import React, { ChangeEvent, Component, useEffect, useMemo, useRef, useState } from 'react'; import useSettings from '../../../hooks/useSettings'; @@ -49,6 +50,8 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { RHFCheckbox } from '../../../components/hook-form'; import { CheckBox } from '@mui/icons-material'; import { CorporateService } from '../../../@types/corporates'; +import { number } from 'yup/lib/locale'; +import DialogLog from './sections/DialogLog'; export default function List() { const { themeStretch } = useSettings(); @@ -56,6 +59,23 @@ export default function List() { const [searchParams, setSearchParams] = useSearchParams(); const [importResult, setImportResult] = useState(null); + const [openDialog, setOpenDialog] = useState(false); + const [dialogTitle, setDialogTitle] = useState(''); + const [isDialog, setIsDialog] = useState(''); + const [edit, setEdit] = useState({}); + const clickHandler = (isDialog: string) => { + switch (isDialog) { + case 'edit': + setIsDialog(isDialog); + setOpenDialog(true); + break; + default: + break; + } + }; + + + // Dummy Default Data const [dataTableIsLoading, setDataTableLoading] = useState(true); const [dataTableLastRequest, setDataTableLastRequest] = useState(0); @@ -151,7 +171,8 @@ export default function List() { axios .put(`/corporates/${corporate_id}/services/${service.service_code}`, { service_code: service.service_code, - status, + status, + reason:service.reason }) .then((res) => { setDataTableData({ @@ -189,7 +210,9 @@ export default function List() { color="success" size="small" onClick={() => { - handleActivate(row, 'inactive'); + // handleActivate(row, 'inactive', 'test'); + clickHandler('edit'); + setEdit({id: row.id, service_code: row.service_code, status: 'inactive'}); }} > Active @@ -201,19 +224,25 @@ export default function List() { color="error" size="small" onClick={() => { - handleActivate(row, 'active'); + clickHandler('edit'); + setEdit({id: row.id, service_code: row.service_code, status: 'active'}); }} > Inactive )} - + + + + {/* COLLAPSIBLE ROW */} @@ -719,6 +748,15 @@ export default function List() { + + {isDialog === 'edit' && ( + + )}
); } diff --git a/frontend/dashboard/src/pages/Corporates/Services/sections/DialogLog.tsx b/frontend/dashboard/src/pages/Corporates/Services/sections/DialogLog.tsx new file mode 100755 index 00000000..9d73c379 --- /dev/null +++ b/frontend/dashboard/src/pages/Corporates/Services/sections/DialogLog.tsx @@ -0,0 +1,213 @@ +import * as Yup from 'yup'; +import { enqueueSnackbar, useSnackbar } from 'notistack'; +import { useNavigate } from 'react-router-dom'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +// @mui +import { styled } from '@mui/material/styles'; +import { LoadingButton } from '@mui/lab'; +import { Box, Button, Grid, Stack, Typography, Chip, Autocomplete } from '@mui/material'; +import { CorporateService } from '../../../../@types/corporates'; +// components +import { FormProvider, RHFTextField, RHFSwitch } from '../../../../components/hook-form'; +import axios from '../../../../utils/axios'; +import { LaravelPaginatedData } from '../../../../@types/paginated-data'; + +// import { Contact } from '../../../../@types/contact'; +import { Link, useParams, useSearchParams } from 'react-router-dom'; + +// @mui +// components +import MuiDialog from '../../../../components/MuiDialog'; +// React +import { ReactElement } from 'react'; + +// ---------------------------------------------------------------------- + +const HeaderStyle = styled('header')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + padding: theme.spacing(2), + justifyContent: 'space-between', +})); +type DataContent = { + info: string; + date: string; + time: string; +}; + +type MuiDialogProps = { + title?: { + name?: string; + icon?: string; + }; + openDialog: boolean; + setOpenDialog: Function; + content?: ReactElement; + data?: DataContent[]; +}; + +type FormValuesProps = { + value: string; + active: boolean; +}; + +// ---------------------------------------------------------------------- + +// ---------------------------------------------------------------------- + +const DialogTopUpLimit = ({ title, openDialog, setOpenDialog, data }: MuiDialogProps) => { + const navigate = useNavigate(); + const [dataTableData, setDataTableData] = useState({ + current_page: 1, + data: [], + path: '', + first_page_url: '', + last_page: 1, + last_page_url: '', + next_page_url: '', + prev_page_url: '', + per_page: 10, + from: 0, + to: 0, + total: 0, + }); + + const { id, service_code, status } = data; + + const isEdit = id ? true : false; + + const NewCorporateSchema = Yup.object().shape({ + reason: Yup.string().required('Corporate Status is required'), + }); + + + const methods = useForm({ + resolver: yupResolver(NewCorporateSchema), + }); + const { + reset, + watch, + handleSubmit, + formState: { isSubmitting }, + } = methods; + + const values = watch(); + useEffect(() => { + if (openDialog === false) { + reset(); + } + }, [openDialog, reset]); + + const { corporate_id } = useParams(); + const handleActivate = (service: any, status: string) => { + axios + .put(`/corporates/${corporate_id}/services/${service.service_code}`, { + service_code: service.service_code, + status, + reason: service.reason + }) + .then((res) => { + // Memuat ulang halaman saat ini + setOpenDialog(false) + window.location.reload(); + // setDataTableData({ + // ...dataTableData, + // data: dataTableData.data.map((service) => { + // let updatedService = service; + // if (id == service.id) { + // updatedService.status = res.data.status; + // } + // return updatedService; + // }), + // }); + }) + .catch((error) => { + console.log(error); + }); + }; + + const onSubmit = async (row : ReturnType) => { + try { + const data = { + service_code : service_code, + reason : row.reason, + id : id, + } + console.log(data) + handleActivate(data, status) + } catch (error: any) { + console.log('data gagal', data); + } + + const ascent = document?.querySelector('ascent'); + if (ascent != null) { + ascent.innerHTML = ''; + } + }; + + function createData(corporateService: CorporateService): CorporateService { + return { + ...corporateService, + }; + } + + const getContent = (props: { row: ReturnType }) => ( + + + + + + + + + + + + + Save + + + + + + + + ); + + return ( + + ); +}; + +export default DialogTopUpLimit; \ No newline at end of file diff --git a/frontend/dashboard/src/pages/Corporates/Services/sections/History.tsx b/frontend/dashboard/src/pages/Corporates/Services/sections/History.tsx new file mode 100644 index 00000000..5e7f7675 --- /dev/null +++ b/frontend/dashboard/src/pages/Corporates/Services/sections/History.tsx @@ -0,0 +1,177 @@ +// @mui +import { + Box, + Button, + Card, + Collapse, + Container, + FormControl, + Grid, + IconButton, + InputLabel, + MenuItem, + OutlinedInput, + Paper, + Select, + SelectChangeEvent, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, + Badge, + Stack, +} from '@mui/material'; +import * as React from 'react'; +import { useParams } from 'react-router-dom'; +import { styled } from '@mui/material/styles'; +import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp'; +import MuiAccordion, { AccordionProps } from '@mui/material/Accordion'; +import { useContext, useEffect, useState } from 'react'; +import MuiAccordionSummary, { + AccordionSummaryProps, +} from '@mui/material/AccordionSummary'; +import useSettings from '../../../../hooks/useSettings'; +import axios from '../../../../utils/axios'; +import { ConfiguredCorporateContext } from '@/contexts/ConfiguredCorporateContext'; +import MuiAccordionDetails from '@mui/material/AccordionDetails'; +import HeaderBreadcrumbs from '../../../../components/HeaderBreadcrumbs'; +import { Corporate } from '@/@types/corporates'; +import { fDate, fDateTime } from '@/utils/formatTime'; + +const Accordion = styled((props: AccordionProps) => ( + +))(({ theme }) => ({ + border: `1px solid ${theme.palette.divider}`, + '&:not(:last-child)': { + borderBottom: 0, + }, + '&:before': { + display: 'none', + }, +})); + +const AccordionSummary = styled((props: AccordionSummaryProps) => ( + } + {...props} + /> +))(({ theme }) => ({ + backgroundColor: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, .05)' + : 'rgba(0, 0, 0, .03)', + flexDirection: 'row-reverse', + '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { + transform: 'rotate(90deg)', + }, + '& .MuiAccordionSummary-content': { + marginLeft: theme.spacing(1), + }, +})); + +const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ + padding: theme.spacing(2), + borderTop: '1px solid rgba(0, 0, 0, .125)', +})); + +export default function CustomizedAccordions() { + const [expanded, setExpanded] = React.useState('panel1'); + + const handleChange = + (panel: string) => (event: React.SyntheticEvent, newExpanded: boolean) => { + setExpanded(newExpanded ? panel : false); + }; + const pageTitle = 'Audittrail Corporate'; + + const { themeStretch } = useSettings(); + + const { corporate_id, service_code } = useParams(); + + const [corporate, setCorporate] = useState(); + const [currentCorporate, setCurrentCorporate ] = useState(); + + const configuredCorporateContext = useContext(ConfiguredCorporateContext); + + useEffect(() => { + setCorporate(configuredCorporateContext.currentCorporate); + const model = 'App\\Models\\CorporateService'; + const url = `/audittrail/${service_code}?model=${model}`; + axios.get(url) + .then((res) => { + setCurrentCorporate(res.data); + }) + .catch((error) => { + console.error('Terjadi kesalahan:', error); + }); + + }, [configuredCorporateContext]); + + return ( +
+ + {currentCorporate?.data.map((item, index) => ( + + + {`Data has ${item.action} by ${item.user_id} on ${fDateTime(item.updated_at)}`} + + + + + Field + Old Value + New Values + + + + {Object.entries(item.old_values).map(([key, value]) => { + let renderedValue; + if (key !== 'reason') { + return null; // Melewati iterasi saat key adalah 'deleted_by' + } + renderedValue = item.new_values[key]; + + const field = key.charAt(0).toUpperCase() + key.slice(1); + + return ( + + {`${field}`} + {`${value}`} + {renderedValue} + + ); + })} + + + + + ))} +
+ ); +} diff --git a/frontend/dashboard/src/routes/index.tsx b/frontend/dashboard/src/routes/index.tsx index 0c7b6acf..78129fe1 100755 --- a/frontend/dashboard/src/routes/index.tsx +++ b/frontend/dashboard/src/routes/index.tsx @@ -11,6 +11,7 @@ import Register from '../pages/auth/Register'; import VerifyCode from '../pages/auth/VerifyCode'; import { AuthProvider } from '../contexts/LaravelAuthContext'; import AuthGuard from '../guards/AuthGuard'; +import { Link, useParams, useSearchParams } from 'react-router-dom'; // ---------------------------------------------------------------------- @@ -104,10 +105,18 @@ export default function Router() { path: ':corporate_id/services/:service_code', element: , }, + { + path: ':corporate_id/services/:service_code/history', + element: , + }, { path: ':corporate_id/plans/create', element: , }, + { + path: ':corporate_id/plans/:plan_id/history', + element: , + }, { path: ':corporate_id/plans', element: , @@ -157,6 +166,10 @@ export default function Router() { path: ':corporate_id/claim-history', element: , }, + { + path: ':corporate_id/corporate-history', + element: , + }, ], }, { @@ -368,6 +381,7 @@ const CorporatePlans = Loadable(lazy(() => import('../pages/Corporates/Corporate const PlanCreate = Loadable(lazy(() => import('../pages/Corporates/Plan/Create'))); const Plans = Loadable(lazy(() => import('../pages/Corporates/Plan/Index'))); +const CorporatePlansHistory = Loadable(lazy(() => import('../pages/Corporates/Plan/sections/History'))); const DiagnosisExclusions = Loadable( lazy(() => import('../pages/Corporates/DiagnosisExclusion/Index')) @@ -398,12 +412,17 @@ const MasterFormulariumCreate = Loadable(lazy(() => import('../pages/Master/Form const CorporateServices = Loadable(lazy(() => import('../pages/Corporates/Services/Index'))); const CorporateServicesCreate = Loadable(lazy(() => import('../pages/Corporates/Services/Create'))); +const CorporateServicesHistory = Loadable(lazy(() => import('../pages/Corporates/Services/sections/History'))); const CorporateHospitals = Loadable(lazy(() => import('../pages/Corporates/Hospital/Index'))); const CorporateClaimHistories = Loadable( lazy(() => import('../pages/Corporates/ClaimHistory/Index')) ); +const CorporateHistories = Loadable( + lazy(() => import('../pages/Corporates/History')) +); + const Profile = Loadable(lazy(() => import('../pages/Profile/Index'))); const Claims = Loadable(lazy(() => import('../pages/Claims/Index'))); diff --git a/frontend/hospital-portal/src/@types/corporates.ts b/frontend/hospital-portal/src/@types/corporates.ts index 03c941d7..44ebe56c 100755 --- a/frontend/hospital-portal/src/@types/corporates.ts +++ b/frontend/hospital-portal/src/@types/corporates.ts @@ -5,6 +5,7 @@ export type Corporate = { code: string; name?: string; welcome_message?: string; + reason?: string; help_text?: string; logo?: any; logo_url?: string;