backend dan penyesian upload file dinamis approval notifikasi

This commit is contained in:
2025-09-11 09:47:42 +07:00
parent 6a4aaff628
commit 3c9066edc6
9 changed files with 375 additions and 24 deletions

View File

@@ -1224,6 +1224,34 @@ class RequestLogController extends Controller
]);
}
}
return Helper::responseJson(data: $request->toArray(), message: 'File Success Uploaded');
}
public function approvalFiles(Request $request, $id)
{
Helper::setCustomPHPIniSettings();
$requestLog = RequestLog::findOrFail($id);
$nominal = $request->nominal;
if($nominal){
$requestLog->nominal = $nominal;
$requestLog->save();
}
if ($request->hasFile('approval_files')) {
foreach ($request->approval_files as $file) {
$fileData = File::storeFile('approval', $id, $file);
$requestLog->files()->updateOrCreate([
'type' => 'approval',
'name' => $fileData['name'],
'original_name' => $file->getClientOriginalName(),
'extension' => $file->getClientOriginalExtension(),
'source' => env('FILESYSTEM_DISK'),
'path' => $fileData['path'],
'created_by' => auth()->user()->id,
'updated_by' => auth()->user()->id,
'reason' => $request->reason,
]);
}
}
return Helper::responseJson(data: $request->toArray(), message: 'File Success Uploaded');
}

View File

@@ -323,6 +323,7 @@ Route::prefix('internal')->group(function () {
Route::post('customer-service/request/exportFiledInvoice', [RequestLogController::class, 'exportFiledInvoice']);
Route::get('customer-service/request/data', [RequestLogController::class, 'generateDataRequestLogExcel']);
Route::post('customer-service/request/{id}/add_file', [RequestLogController::class, 'requestFiles']);
Route::post('customer-service/request/{id}/approval_files', [RequestLogController::class, 'approvalFiles']);
Route::post('customer-service/request/{id}/delete_file', [RequestLogController::class, 'deleteFiles']);
Route::post('customer-service/request/final-log', [RequestLogController::class, 'updateFinalLog']);

View File

@@ -174,6 +174,7 @@ class RequestLogShowResource extends JsonResource
'keterangan' => $requestLog['keterangan'],
'hak_kamar_pasien' => $requestLog['hak_kamar_pasien'],
'penempatan_kamar' => $requestLog['penempatan_kamar'],
'nominal' => $requestLog['nominal'],
'catatan' => $requestLog['catatan'],
'reason' => $requestLog['reason'],
'diagnosis' => $icd,

View File

@@ -111,28 +111,62 @@ class File extends Model
// return $path;
// }
// public static function storeFile($type, $id, $file)
// {
// // Pastikan directory tidak punya trailing slash
// $directory = rtrim(self::getDirectory($type), '/');
// // Buat nama file yang unik dan aman
// $originalName = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
// $extension = $file->getClientOriginalExtension();
// $safeName = Str::slug($originalName);
// $uniqueName = $safeName . '-' . uniqid() . '.' . $extension;
// // Upload file ke disk 's3' dengan visibility 'public'
// $path = Storage::disk('s3')->putFileAs(
// $directory,
// $file,
// $uniqueName,
// 'public'
// );
// // Kembalikan path dan nama unik agar bisa digunakan di controller
// return [
// 'path' => $directory . '/' . $uniqueName, // hasil konsisten
// 'name' => $uniqueName,
// ];
// }
public static function storeFile($type, $id, $file)
{
// Pastikan directory tidak punya trailing slash
// 1. Ambil nama disk dari konfigurasi default
// Nilainya akan 'public', 'local', atau 's3' tergantung .env Anda
$disk = config('filesystems.default');
$directory = rtrim(self::getDirectory($type), '/');
// Buat nama file yang unik dan aman
// Buat nama file yang unik dan aman (kode Anda sudah bagus)
$originalName = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
$extension = $file->getClientOriginalExtension();
$safeName = Str::slug($originalName);
$uniqueName = $safeName . '-' . uniqid() . '.' . $extension;
// Upload file ke disk 's3' dengan visibility 'public'
$path = Storage::disk('s3')->putFileAs(
$directory,
$file,
$uniqueName,
'public'
// 2. Gunakan disk yang sudah dinamis
$path = Storage::disk($disk)->putFileAs(
$directory,
$file,
$uniqueName
);
// Kembalikan path dan nama unik agar bisa digunakan di controller
// 3. (Sangat Direkomendasikan) Tambahkan penanganan error
if ($path === false) {
Log::error("Gagal menyimpan file ke disk '{$disk}' pada path: {$directory}/{$uniqueName}");
return false; // Kembalikan false jika upload gagal
}
// 4. Kembalikan path asli dari hasil upload untuk konsistensi
return [
'path' => $directory . '/' . $uniqueName, // hasil konsisten
'path' => $path, // Gunakan $path yang dikembalikan oleh Storage
'name' => $uniqueName,
];
}

View File

@@ -53,7 +53,8 @@ class RequestLog extends Model
'created_final_by',
'specialities_id',
'dppj',
'type_of_member'
'type_of_member',
'nominal',
];
protected $hidden = [

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('request_logs', function (Blueprint $table) {
$table->integer('nominal')->default(0)->after('total_cob');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('table_request_log', function (Blueprint $table) {
$table->dropColumn('nominal');
});
}
};

View File

@@ -0,0 +1,185 @@
import { Stack, Typography, Button, Paper, Grid, IconButton, TextField } from "@mui/material";
import MuiDialog from "@/components/MuiDialog";
import { fDate, fDateTimesecond } from '@/utils/formatTime';
import { ContentCopy, WhatsApp, Instagram, Facebook, Telegram } from "@mui/icons-material";
type DialogConfirmationType = {
openDialog: boolean;
setOpenDialog: any;
onSubmit?: void;
requestLog: any;
shareLink: boolean;
};
export default function DialogSendWa({
requestLog,
setOpenDialog,
openDialog,
shareLink = false,
}: DialogConfirmationType) {
const data = {
provider: requestLog?.provider || "LOG",
memberId: requestLog?.member_id || "-",
policyNumber: requestLog?.policy_number || "-",
name: requestLog?.name || "-",
submissionDate: requestLog?.submission_date ? fDateTimesecond(requestLog?.submission_date) : "-",
claimMethod: requestLog?.claim_method || "-",
serviceType: requestLog?.service_type || "-",
linkApproval: requestLog?.url_approval || "https://example.com/approval-link",
};
const getContent = () => (
<Stack spacing={2} sx={{ marginTop: 2, padding: 2 }}>
<Typography>Are you sure want to send this request ?</Typography>
<Paper variant="outlined" sx={{ p: 2 }}>
<Grid container spacing={1}>
<Grid item xs={5}>
<Typography variant="body2" color="textSecondary">
Member ID
</Typography>
</Grid>
<Grid item xs={7}>
<Typography>{data.memberId}</Typography>
</Grid>
<Grid item xs={5}>
<Typography variant="body2" color="textSecondary">
Policy Number
</Typography>
</Grid>
<Grid item xs={7}>
<Typography fontWeight="bold">{data.policyNumber}</Typography>
</Grid>
<Grid item xs={5}>
<Typography variant="body2" color="textSecondary">
Name
</Typography>
</Grid>
<Grid item xs={7}>
<Typography>{data.name}</Typography>
</Grid>
<Grid item xs={5}>
<Typography variant="body2" color="textSecondary">
Submission Date
</Typography>
</Grid>
<Grid item xs={7}>
<Typography>{data.submissionDate}</Typography>
</Grid>
<Grid item xs={5}>
<Typography variant="body2" color="textSecondary">
Claim Method
</Typography>
</Grid>
<Grid item xs={7}>
<Typography>{data.claimMethod}</Typography>
</Grid>
<Grid item xs={5}>
<Typography variant="body2" color="textSecondary">
Service Type
</Typography>
</Grid>
<Grid item xs={7}>
<Typography>{data.serviceType}</Typography>
</Grid>
</Grid>
</Paper>
{shareLink ? (
<>
<Typography>Share this link only with authorized parties!</Typography>
{/* <Stack direction="row" spacing={2}>
<IconButton color="success">
<WhatsApp />
</IconButton>
<IconButton color="primary">
<Instagram />
</IconButton>
<IconButton color="primary">
<Telegram />
</IconButton>
<IconButton color="primary">
<Facebook />
</IconButton>
</Stack> */}
<Typography variant="body2">or copy link</Typography>
<Stack direction="row" spacing={1}>
<TextField
fullWidth
size="small"
value={data.linkApproval}
InputProps={{
readOnly: true,
}}
/>
<Button
variant="outlined"
onClick={() => navigator.clipboard.writeText(data.linkApproval)}
>
Copy
</Button>
</Stack>
</>
): null }
</Stack>
);
const getAction = () => {
if (shareLink) {
return (
<Stack direction="row" justifyContent="flex-end">
<Button variant="outlined" onClick={() => setOpenDialog(false)}>
Cancel
</Button>
</Stack>
);
}
const handleSend = () => {
const message = `*Request Approval*
Yth. Bapak/Ibu, Nama Penerima
Mohon persetujuan atas data berikut:
Provider: *${data.provider}*
Member ID: ${data.memberId}
Nama: ${data.name}
Policy Number: ${data.policyNumber}
Submission Date: ${data.submissionDate}
Claim Method: ${data.claimMethod}
Service Type: ${data.serviceType}
Silakan klik link berikut untuk approval:
${data.linkApproval}`;
const encodedMessage = encodeURIComponent(message);
const waUrl = `https://wa.me/6283807417196?text=${encodedMessage}`;
window.open(waUrl, "_blank");
};
return (
<Stack direction="row" justifyContent="space-between" spacing={2}>
<Button variant="outlined" onClick={() => setOpenDialog(false)}>
Cancel
</Button>
<Button variant="contained" onClick={handleSend}>
Send
</Button>
</Stack>
);
};
return (
<MuiDialog
title={{ name: "Confirmation", variant: "h4" }}
openDialog={openDialog}
setOpenDialog={setOpenDialog}
content={getContent()}
action={getAction()}
maxWidth="sm"
/>
);
}

View File

@@ -30,6 +30,7 @@ import { useFieldArray, useForm } from 'react-hook-form';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import { useEffect, useState, useRef, useMemo } from 'react';
import axios from '../../../utils/axios';
import { enqueueSnackbar } from 'notistack';
// pages
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
import { DetailFinalLogType } from './Model/Types';
@@ -43,6 +44,7 @@ import { Delete, EditOutlined, ExpandMore } from '@mui/icons-material';
import {BenefitData } from '../FinalLog/Model/Types'
import AddIcon from '@mui/icons-material/Add';
import { LoadingButton } from '@mui/lab';
import { makeFormData } from '@/utils/jsonToFormData';
// Import Card Detail Final LOG
import CardDetail from '../Components/CardDetail';
@@ -67,6 +69,8 @@ import CardFile from '../Components/CardFile';
import DialogEditFinalLOG from './Components/DialogEditFinalLOG';
import DialogDeleteFileLog from './Components/DialogDeleteFileLog';
import DialogUploadFileFinalLog from './Components/DialogUploadFileFinalLog';
import DialogSendWa from './Components/DialogSendWa';
import { set } from 'nprogress';
// ----------------------------------------------------------------------
@@ -79,6 +83,7 @@ export default function Detail() {
const { themeStretch } = useSettings();
const [requestLog, setRequestLog] = useState<DetailFinalLogType>();
const [isReversal, setIsReversal] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false);
const defaultValues: any = {nominal : 0};
const validationSchema = Yup.object().shape({nominal: Yup.number().typeError('Nominal harus berupa angka').required('Nominal harus diisi')})
@@ -91,7 +96,25 @@ export default function Detail() {
const { handleSubmit, reset, watch, setValue, formState: { isDirty, isSubmitting, errors } } = methods;
const onSubmit = async (data: any) => {
alert('Nominal: ' + data.nominal);
setSubmitLoading(true);
const formData = makeFormData({
request_logs_id: id,
approval_files: fileApprovals,
nominal: data.nominal,
});
axios
.post(`/customer-service/request/${id}/approval_files`, formData)
.then((response) => {
enqueueSnackbar('Berhasil membuat data', { variant: 'success' });
window.location.reload()
})
.catch(({ response }) => {
enqueueSnackbar('Something Went Wrong', { variant: 'error' });
})
.then(() => {
setSubmitLoading(false);
});
}
const { id } = useParams();
@@ -130,6 +153,8 @@ export default function Detail() {
const [openDialogEditDetail, setDialogDEditDetail] = useState(false);
const [openDialogBenefit, setDialogBenefit] = useState(false);
const [openDialogMedicine, setDialogMedicine] = useState(false);
const [openDialogSendWa, setDialogSendWa] = useState(false);
const [shareLink, setShareLink] = useState(false);
// Handel Delete Detail Benefit
const [idBenefitData, setIdBenefitData] = useState<number>();
@@ -175,16 +200,16 @@ export default function Detail() {
const fileDiagnosaInput = useRef<HTMLInputElement>(null);
const [fileDiagnosas, setFileDiagnosas] = useState<any>([]);
const [fileApprovals, setFileApproval] = useState<any>([]);
const handleDiagnosaInputChange = (event:any) => {
if (event.target.files[0]) {
setFileDiagnosas([...fileDiagnosas, ...event.target.files]);
setFileApproval([...fileApprovals, ...event.target.files]);
} else {
console.log('NO FILE');
}
};
const removeDiagnosaFiles = (filesState:any, index:any) => {
setFileDiagnosas(
const removeApprovalFiles = (filesState:any, index:any) => {
setFileApproval(
filesState.filter((file:any, fileIndex:any) => {
return fileIndex != index;
})
@@ -381,7 +406,7 @@ export default function Detail() {
Upload Tindakan Persetujuan
</Typography>
{fileDiagnosas?.map((file: any, index: number) => (
{fileApprovals?.map((file: any, index: number) => (
<Stack
key={index}
direction="row"
@@ -395,7 +420,7 @@ export default function Detail() {
icon="eva:trash-2-outline"
color="darkred"
sx={{ cursor: "pointer" }}
onClick={() => removeDiagnosaFiles(fileDiagnosas, index)}
onClick={() => removeApprovalFiles(fileApprovals, index)}
/>
</Stack>
))}
@@ -438,29 +463,60 @@ export default function Detail() {
label="Nominal"
required
placeholder="Nominal"
value={requestLog?.nominal || 0}
/>
<LoadingButton
{/* <LoadingButton
type="submit" // ✅ supaya ikut submit
variant="contained"
sx={{ marginTop: 2, p: 2, backgroundColor: "#19BBBB" }}
loading={false}
>
Simpan
</LoadingButton>
</LoadingButton> */}
<Stack direction="row" spacing={2} sx={{ mt: 6 }}>
{/* TOMBOL SIMPAN DI KIRI */}
<LoadingButton
type="submit"
variant="contained"
sx={{ p: 2, backgroundColor: "#19BBBB" }}
loading={false}
size='small'
>
Simpan
</LoadingButton>
{/* Ini adalah spacer untuk mendorong tombol berikutnya ke kanan */}
<Box sx={{ flexGrow: 1 }} />
{/* GRUP TOMBOL DI KANAN */}
<Stack direction="row" spacing={1.5} mt={2}>
<Button variant="contained" size="small" sx={{ p: 2, backgroundColor: "#19BBBB" }}
onClick={() => {setDialogSendWa(true); setShareLink(false); }}>
Kirim (WA Chatbot)
</Button>
<Button variant="contained" size="small" sx={{ p: 2, backgroundColor: "#19BBBB" }}
onClick={() => {setDialogSendWa(true); setShareLink(true); }}>
Share Link
</Button>
</Stack>
</Stack>
</FormProvider>
</Stack>
</FormProvider>
)}
{/* FILE YANG SUDAH TERUPLOAD */}
{requestLog?.files?.map((documentType, index) => (
{requestLog?.files
?.filter((document) => document.type === 'approval')
?.map((documentType, index) => (
<Stack
key={index}
direction="row"
justifyContent="space-between"
alignItems="center"
sx={{ mb: 2 }}
sx={{ mt: 2 }}
>
<a
href={documentType.url}
@@ -778,6 +834,16 @@ export default function Detail() {
requestLog={requestLog}
openDialog={openDialogEditDetail}
/>
<DialogSendWa
requestLog={requestLog}
openDialog={openDialogSendWa}
setOpenDialog={setDialogSendWa}
shareLink={shareLink}
/>
</Grid>
{/* Medicine */}
@@ -843,7 +909,9 @@ export default function Detail() {
) : null }
</Stack>
{requestLog?.files?.map((documentType, index) => (
{requestLog?.files
?.filter((document) => document.type !== 'approval')
?.map((documentType, index) => (
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{marginBottom: 2}} key={index}>
<Stack direction="column" spacing={2} >
<a

View File

@@ -66,6 +66,7 @@ export type DetailFinalLogType = {
files : file[],
member_usage_benefit : number,
corporate_id : number
nominal : number
}
export type Diagnosis = {