lavoce #11

Merged
bragaz_rexita merged 8 commits from lavoce into main 2025-10-28 09:47:36 +00:00
14 changed files with 1648 additions and 951 deletions

View File

@@ -5,9 +5,18 @@ const API_BASE_URL = import.meta.env.VITE_API_SERVER;
// Get file from uploads directory
const getFile = async (folder, filename) => {
const token = localStorage.getItem('token');
if (!token) {
throw new Error('No authentication token found');
}
const response = await axios.get(`${API_BASE_URL}/file-uploads/${folder}/${encodeURIComponent(filename)}`, {
responseType: 'blob'
responseType: 'blob',
headers: {
'Authorization': `Bearer ${token.replace(/"/g, '')}`
}
});
return response.data;
};

View File

@@ -3,7 +3,7 @@ import { SendRequest } from '../components/Global/ApiRequest';
const getAllJadwalShift = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `jadwal-shift?${queryParams.toString()}`,
prefix: `user-schedule?${queryParams.toString()}`,
});
return response.data;
@@ -12,7 +12,7 @@ const getAllJadwalShift = async (queryParams) => {
const getJadwalShiftById = async (id) => {
const response = await SendRequest({
method: 'get',
prefix: `jadwal-shift/${id}`,
prefix: `user-schedule/${id}`,
});
return response.data;
@@ -21,7 +21,7 @@ const getJadwalShiftById = async (id) => {
const createJadwalShift = async (queryParams) => {
const response = await SendRequest({
method: 'post',
prefix: `jadwal-shift`,
prefix: `user-schedule`,
params: queryParams,
});
@@ -31,7 +31,7 @@ const createJadwalShift = async (queryParams) => {
const updateJadwalShift = async (id, queryParams) => {
const response = await SendRequest({
method: 'put',
prefix: `jadwal-shift/${id}`,
prefix: `user-schedule/${id}`,
params: queryParams,
});
@@ -41,7 +41,7 @@ const updateJadwalShift = async (id, queryParams) => {
const deleteJadwalShift = async (id) => {
const response = await SendRequest({
method: 'delete',
prefix: `jadwal-shift/${id}`,
prefix: `user-schedule/${id}`,
});
return response.data;
};

View File

@@ -17,7 +17,7 @@ const LayoutSidebar = () => {
// console.log(collapsed, type);
}}
style={{
background: 'linear-gradient(180deg, #FF8C42 0%, #FF6B35 100%)',
background: 'linear-gradient(180deg, #1BAA56 0%,rgb(5, 75, 34) 100%)',
overflow: 'auto',
height: '100vh',
position: 'fixed',

View File

@@ -22,18 +22,18 @@ const ListHistoryEvent = memo(function ListHistoryEvent(props) {
},
{
title: 'Tag Name',
dataIndex: 'tag_name',
key: 'tag_name',
dataIndex: 'tagname',
key: 'tagname',
width: '40%',
},
{
title: 'Description',
dataIndex: 'condition',
key: 'condition',
dataIndex: 'description',
key: 'description',
width: '20%',
render: (_, record) => (
<Button type="text" style={{ backgroundColor: record.status_color, width: '100%' }}>
{record.condition}
{record.description}
</Button>
),
},

View File

@@ -1,173 +1,278 @@
import React, { useEffect, useState } from 'react';
import {
Modal,
Typography,
Button,
ConfigProvider,
Form,
Select,
Spin,
Input
} from 'antd';
import { NotifOk, NotifAlert } from '../../../components/Global/ToastNotif';
import { updateJadwalShift, createJadwalShift } from '../../../api/jadwal-shift.jsx';
import { Modal, Select, Typography, Button, ConfigProvider } from 'antd';
import { NotifOk } from '../../../components/Global/ToastNotif';
import { createJadwalShift, updateJadwalShift } from '../../../api/jadwal-shift';
import { getAllUser } from '../../../api/user';
import { getAllShift } from '../../../api/master-shift';
import { validateRun } from '../../../Utils/validate';
const { Text } = Typography;
const { Option } = Select;
const DetailJadwalShift = (props) => {
const [form] = Form.useForm();
const [confirmLoading, setConfirmLoading] = useState(false);
const [employees, setEmployees] = useState([]);
const [loadingEmployees, setLoadingEmployees] = useState(false);
const [shifts, setShifts] = useState([]);
const [loadingData, setLoadingData] = useState(false);
const isReadOnly = props.actionMode === 'preview';
const defaultData = {
id: '',
user_id: null,
shift_id: null,
schedule_id: '',
user_phone: null,
};
const [formData, setFormData] = useState(defaultData);
const handleSelectChange = (name, value) => {
const updates = { [name]: value };
if (name === 'user_id') {
const selectedEmployee = employees.find((emp) => emp.user_id === value);
updates.user_phone = selectedEmployee?.user_phone || '-';
}
setFormData({
...formData,
...updates,
});
};
const handleCancel = () => {
props.setSelectedData(null);
props.setActionMode('list');
};
const fetchEmployees = async () => {
setLoadingEmployees(true);
const fetchData = async () => {
setLoadingData(true);
try {
// Data dummy untuk dropdown karyawan
const dummyEmployees = [
{ employee_id: '101', nama_employee: 'Andi Pratama' },
{ employee_id: '102', nama_employee: 'Budi Santoso' },
{ employee_id: '103', nama_employee: 'Citra Lestari' },
{ employee_id: '104', nama_employee: 'Dewi Anggraini' },
{ employee_id: '105', nama_employee: 'Eko Wahyudi' },
{ employee_id: '106', nama_employee: 'Fitriani' },
];
setEmployees(dummyEmployees);
const params = new URLSearchParams({
page: 1,
limit: 100,
});
const [usersResponse, shiftsResponse] = await Promise.all([
getAllUser(params),
getAllShift(params),
]);
const userData = usersResponse?.data || usersResponse || [];
const shiftData = shiftsResponse?.data || shiftsResponse || [];
setEmployees(Array.isArray(userData) ? userData : []);
setShifts(Array.isArray(shiftData) ? shiftData : []);
} catch (error) {
NotifAlert({ icon: 'error', title: 'Gagal', message: 'Gagal memuat daftar karyawan.' });
NotifOk({
icon: 'error',
title: 'Gagal',
message: 'Gagal memuat data karyawan atau shift.',
});
} finally {
setLoadingEmployees(false);
setLoadingData(false);
}
};
const handleSave = async () => {
setConfirmLoading(true);
// Daftar aturan validasi
const validationRules = [
{ field: 'user_id', label: 'Nama Karyawan', required: true },
{ field: 'shift_id', label: 'Shift', required: true },
];
if (
validateRun(formData, validationRules, (errorMessages) => {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: errorMessages,
});
setConfirmLoading(false);
})
)
return;
try {
const values = await form.validateFields();
let payload;
let responseMessage;
const payload = {
user_id: formData.user_id,
shift_id: formData.shift_id,
};
setConfirmLoading(true);
if (props.actionMode === 'edit') {
payload = { ...props.selectedData, ...values };
// await updateJadwalShift(payload.schedule_id, payload);
console.log("Updating schedule with payload:", payload);
responseMessage = 'Jadwal berhasil diperbarui.';
} else { // 'add' mode
payload = {
employee_id: values.employee_id,
shift_name: props.selectedData.shift_name,
schedule_date: new Date().toISOString().split('T')[0], // Example date
};
// await createJadwalShift(payload);
console.log("Creating schedule with payload:", payload);
responseMessage = 'User berhasil ditambahkan ke jadwal.';
// Add schedule_id only if editing and it exists
if (props.actionMode === 'edit' && formData.schedule_id) {
payload.schedule_id = formData.schedule_id;
}
await new Promise(resolve => setTimeout(resolve, 500)); // Simulasi API call
NotifOk({ icon: 'success', title: 'Berhasil', message: responseMessage });
props.setActionMode('list'); // Menutup modal dan memicu refresh di parent
const response =
props.actionMode === 'edit'
? await updateJadwalShift(formData.id, payload)
: await createJadwalShift(payload);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
const action = props.actionMode === 'edit' ? 'diubah' : 'ditambahkan';
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `Jadwal berhasil ${action}.`,
});
props.setActionMode('list');
} else {
NotifOk({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Terjadi kesalahan saat menyimpan data.',
});
}
} catch (error) {
const message = error.response?.data?.message || 'Gagal memperbarui jadwal.';
NotifAlert({ icon: 'error', title: 'Gagal', message });
NotifOk({
icon: 'error',
title: 'Error',
message: error.message || 'Terjadi kesalahan pada server.',
});
} finally {
setConfirmLoading(false);
}
};
useEffect(() => {
// Hanya jalankan jika modal untuk 'edit' atau 'preview' terbuka
if (props.showModal) {
fetchEmployees();
if (props.actionMode === 'edit' || props.actionMode === 'preview') {
form.setFieldsValue({
employee_id: props.selectedData.employee_id,
shift_name: props.selectedData.shift_name,
});
} else if (props.actionMode === 'add') {
form.setFieldsValue({
shift_name: props.selectedData.shift_name,
employee_id: null, // Reset employee selection
});
}
fetchData();
}
}, [props.actionMode, props.showModal, props.selectedData, form]);
if (props.selectedData) {
setFormData({
id: props.selectedData.id || '',
user_id: props.selectedData.user_id || null,
shift_id: props.selectedData.shift_id || null,
schedule_id: props.selectedData.schedule_id || '',
user_phone: props.selectedData.whatsapp || props.selectedData.user_phone || null,
});
} else {
setFormData(defaultData);
}
}, [props.showModal, props.selectedData, props.actionMode]);
return (
<Modal
title={isReadOnly ? 'Preview Jadwal' : (props.actionMode === 'edit' ? 'Edit Jadwal' : 'Tambah User')}
title={`${
props.actionMode === 'add'
? 'Tambah'
: props.actionMode === 'preview'
? 'Preview'
: 'Edit'
} Jadwal Shift`}
open={props.showModal}
onCancel={handleCancel}
width={600}
footer={[
<React.Fragment key="modal-footer">
<Button key="back" onClick={handleCancel}>
{isReadOnly ? 'Tutup' : 'Batal'}
</Button>
{!isReadOnly && (
<Button key="submit" type="primary" loading={confirmLoading} onClick={handleSave} style={{ backgroundColor: '#23A55A' }}>
Simpan
</Button>
)}
<ConfigProvider
theme={{
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
},
},
}}
>
<Button onClick={handleCancel}>{props.readOnly ? 'Tutup' : 'Batal'}</Button>
</ConfigProvider>
<ConfigProvider
theme={{
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
},
},
}}
>
{!props.readOnly && (
<Button loading={confirmLoading} onClick={handleSave}>
Simpan
</Button>
)}
</ConfigProvider>
</React.Fragment>,
]}
>
<Spin spinning={loadingEmployees} tip="Memuat data...">
<Form form={form} layout="vertical" name="shift_form">
{props.actionMode === 'add' ? (
<>
<Form.Item
name="shift_name"
label="Shift"
>
<Input disabled />
</Form.Item>
<Form.Item
name="employee_id"
label="Nama Karyawan"
rules={[{ required: true, message: 'Nama karyawan wajib dipilih!' }]}
>
<Select
placeholder="Pilih karyawan"
showSearch
optionFilterProp="children"
>
{employees.map(emp => (
<Option key={emp.employee_id} value={emp.employee_id}>{emp.nama_employee}</Option>
))}
</Select>
</Form.Item>
</>
) : (
<>
<Form.Item
name="employee_id"
label="Nama Karyawan"
rules={[{ required: true, message: 'Nama karyawan wajib dipilih!' }]}
>
<Select placeholder="Pilih karyawan" disabled={isReadOnly} showSearch optionFilterProp="children">
{employees.map(emp => (
<Option key={emp.employee_id} value={emp.employee_id}>{emp.nama_employee}</Option>
))}
</Select>
</Form.Item>
<Form.Item name="shift_name" label="Shift" rules={[{ required: true, message: 'Shift wajib dipilih!' }]}>
<Select placeholder="Pilih shift" disabled={isReadOnly}>
<Option value="PAGI">PAGI</Option>
<Option value="SIANG">SIANG</Option>
<Option value="MALAM">MALAM</Option>
</Select>
</Form.Item>
</>
)}
</Form>
</Spin>
{formData && (
<div>
<div style={{ marginBottom: 12 }}>
<Text strong>Nama Karyawan</Text>
<Text style={{ color: 'red' }}> *</Text>
<Select
value={formData.user_id}
onChange={(value) => handleSelectChange('user_id', value)}
placeholder="Pilih karyawan"
disabled={props.readOnly || loadingData}
loading={loadingData}
showSearch
optionFilterProp="children"
filterOption={(input, option) =>
option?.children?.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
style={{ width: '100%' }}
>
{employees
.filter((emp) => emp.user_id != null)
.map((emp) => (
<Option key={`emp-${emp.user_id}`} value={emp.user_id}>
{emp.user_fullname || emp.user_name}
</Option>
))}
</Select>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>No. Telepon</Text>
<div
style={{
padding: '8px 12px',
backgroundColor: '#f5f5f5',
borderRadius: '6px',
marginTop: '4px',
color: formData.user_phone ? '#000' : '#999',
}}
>
{formData.user_phone || 'Pilih karyawan terlebih dahulu'}
</div>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Shift</Text>
<Text style={{ color: 'red' }}> *</Text>
<Select
value={formData.shift_id}
onChange={(value) => handleSelectChange('shift_id', value)}
placeholder="Pilih shift"
disabled={props.readOnly || loadingData}
loading={loadingData}
showSearch
optionFilterProp="children"
filterOption={(input, option) =>
option?.children?.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
style={{ width: '100%' }}
>
{shifts
.filter((shift) => shift.shift_id != null)
.map((shift) => (
<Option key={`shift-${shift.shift_id}`} value={shift.shift_id}>
{shift.shift_name}
</Option>
))}
</Select>
</div>
</div>
)}
</Modal>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,7 @@ const AddBrandDevice = () => {
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState(defaultData);
const [errorCodes, setErrorCodes] = useState([]);
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
const {
solutionFields,
@@ -49,16 +50,29 @@ const AddBrandDevice = () => {
handleSolutionStatusChange,
resetSolutionFields,
checkFirstSolutionValid,
setSolutionsForExistingRecord
setSolutionsForExistingRecord,
} = useErrorCodeLogic(errorCodeForm, fileList);
useEffect(() => {
setBreadcrumbItems([
{ title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}> Master</span> },
{
title: <span style={{ fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }} onClick={() => navigate('/master/brand-device')}>Brand Device</span>
title: (
<span
style={{ fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}
onClick={() => navigate('/master/brand-device')}
>
Brand Device
</span>
),
},
{
title: (
<span style={{ fontSize: '14px', fontWeight: 'bold' }}>
Tambah Brand Device
</span>
),
},
{ title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}>Tambah Brand Device</span> }
]);
}, [setBreadcrumbItems, navigate]);
@@ -71,25 +85,31 @@ const AddBrandDevice = () => {
await brandForm.validateFields();
setCurrentStep(1);
} catch (error) {
NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk brand device!' });
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Harap isi semua kolom wajib untuk brand device!',
});
}
};
const handleFinish = async () => {
setConfirmLoading(true);
try {
const transformedErrorCodes = errorCodes.map(ec => ({
const transformedErrorCodes = errorCodes.map((ec) => ({
error_code: ec.error_code,
error_code_name: ec.error_code_name || '',
error_code_description: ec.error_code_description || '',
error_code_color: ec.error_code_color || '#000000',
path_icon: ec.path_icon || '',
is_active: ec.status !== undefined ? ec.status : true,
solution: (ec.solution || []).map(sol => ({
solution: (ec.solution || []).map((sol) => ({
solution_name: sol.solution_name,
type_solution: sol.type_solution,
text_solution: sol.text_solution || '',
path_solution: sol.path_solution || '',
is_active: sol.is_active !== false
}))
is_active: sol.is_active !== false,
})),
}));
const finalFormData = {
@@ -98,23 +118,28 @@ const AddBrandDevice = () => {
brand_model: formData.brand_model || '',
brand_manufacture: formData.brand_manufacture,
is_active: formData.is_active,
error_code: transformedErrorCodes.length > 0 ? transformedErrorCodes : [
{
error_code: "DEFAULT",
error_code_name: "Default Error Code",
error_code_description: "Default error description",
is_active: true,
solution: [
{
solution_name: "Default Solution",
type_solution: "text",
text_solution: "Default solution text",
path_solution: "",
is_active: true
}
]
}
]
error_code:
transformedErrorCodes.length > 0
? transformedErrorCodes
: [
{
error_code: 'DEFAULT',
error_code_name: 'Default Error Code',
error_code_description: 'Default error description',
error_code_color: '#000000',
path_icon: '',
is_active: true,
solution: [
{
solution_name: 'Default Solution',
type_solution: 'text',
text_solution: 'Default solution text',
path_solution: '',
is_active: true,
},
],
},
],
};
const response = await createBrand(finalFormData);
@@ -135,9 +160,9 @@ const AddBrandDevice = () => {
}
} catch (error) {
NotifAlert({
icon: "error",
title: "Gagal",
message: error.message || "Gagal menyimpan data. Silakan coba lagi.",
icon: 'error',
title: 'Gagal',
message: error.message || 'Gagal menyimpan data. Silakan coba lagi.',
});
} finally {
setConfirmLoading(false);
@@ -149,9 +174,11 @@ const AddBrandDevice = () => {
error_code: record.error_code,
error_code_name: record.error_code_name,
error_code_description: record.error_code_description,
error_code_color: record.error_code_color,
status: record.status,
});
setFileList(record.fileList || []);
setErrorCodeIcon(record.errorCodeIcon || null);
setIsErrorCodeFormReadOnly(true);
setEditingErrorCodeKey(null);
@@ -165,9 +192,11 @@ const AddBrandDevice = () => {
error_code: record.error_code,
error_code_name: record.error_code_name,
error_code_description: record.error_code_description,
error_code_color: record.error_code_color,
status: record.status,
});
setFileList(record.fileList || []);
setErrorCodeIcon(record.errorCodeIcon || null);
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(record.key);
@@ -177,21 +206,29 @@ const AddBrandDevice = () => {
};
const handleAddErrorCode = async (newErrorCode) => {
// Include the current icon in the error code
const errorCodeWithIcon = {
...newErrorCode,
errorCodeIcon: errorCodeIcon
};
if (editingErrorCodeKey) {
const updatedCodes = errorCodes.map(item => item.key === editingErrorCodeKey ? newErrorCode : item);
const updatedCodes = errorCodes.map((item) =>
item.key === editingErrorCodeKey ? errorCodeWithIcon : item
);
setErrorCodes(updatedCodes);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil diupdate!'
message: 'Error code berhasil diupdate!',
});
} else {
const updatedCodes = [...errorCodes, newErrorCode];
const updatedCodes = [...errorCodes, errorCodeWithIcon];
setErrorCodes(updatedCodes);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil ditambahkan!'
message: 'Error code berhasil ditambahkan!',
});
}
@@ -203,9 +240,10 @@ const AddBrandDevice = () => {
errorCodeForm.setFieldsValue({
status: true,
solution_status_0: true,
solution_type_0: 'text'
solution_type_0: 'text',
});
setFileList([]);
setErrorCodeIcon(null);
resetSolutionFields();
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(null);
@@ -220,16 +258,16 @@ const AddBrandDevice = () => {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap brand harus memiliki minimal 1 error code!'
message: 'Setiap brand harus memiliki minimal 1 error code!',
});
return;
}
setErrorCodes(errorCodes.filter(item => item.key !== key));
setErrorCodes(errorCodes.filter((item) => item.key !== key));
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil dihapus!'
message: 'Error code berhasil dihapus!',
});
};
@@ -248,12 +286,17 @@ const AddBrandDevice = () => {
const handleSolutionFileUpload = async (file) => {
try {
const isAllowedType = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif'].includes(file.type);
const isAllowedType = [
'application/pdf',
'image/jpeg',
'image/png',
'image/gif',
].includes(file.type);
if (!isAllowedType) {
NotifAlert({
icon: 'error',
title: 'Error',
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`,
});
return;
}
@@ -271,17 +314,17 @@ const AddBrandDevice = () => {
file.solution_name = file.name;
file.solutionId = solutionFields[0];
file.type_solution = fileType;
setFileList(prevList => [...prevList, file]);
setFileList((prevList) => [...prevList, file]);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `${file.name} berhasil diupload!`
message: `${file.name} berhasil diupload!`,
});
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: `Gagal mengupload ${file.name}`
message: `Gagal mengupload ${file.name}`,
});
}
} catch (error) {
@@ -289,23 +332,33 @@ const AddBrandDevice = () => {
NotifAlert({
icon: 'error',
title: 'Error',
message: `Gagal mengupload ${file.name}. Silakan coba lagi.`
message: `Gagal mengupload ${file.name}. Silakan coba lagi.`,
});
}
};
const handleFileRemove = (file) => {
const newFileList = fileList.filter(item => item.uid !== file.uid);
const newFileList = fileList.filter((item) => item.uid !== file.uid);
setFileList(newFileList);
};
const handleErrorCodeIconUpload = (iconData) => {
setErrorCodeIcon(iconData);
};
const handleErrorCodeIconRemove = () => {
setErrorCodeIcon(null);
};
const renderStepContent = () => {
if (currentStep === 0) {
return (
<BrandForm
form={brandForm}
formData={formData}
onValuesChange={(changedValues, allValues) => setFormData(prev => ({...prev, ...allValues}))}
onValuesChange={(changedValues, allValues) =>
setFormData((prev) => ({ ...prev, ...allValues }))
}
isEdit={false}
/>
);
@@ -314,17 +367,22 @@ const AddBrandDevice = () => {
if (currentStep === 1) {
return (
<Row gutter={24}>
<Col span={12}>
<Col span={8}>
<Title level={5} style={{ marginBottom: 16 }}>
{isErrorCodeFormReadOnly
? 'View Error Code'
: (editingErrorCodeKey ? 'Edit Error Code' : 'Tambah Error Code')
}
: editingErrorCodeKey
? 'Edit Error Code'
: 'Tambah Error Code'}
</Title>
<Form
form={errorCodeForm}
layout="vertical"
initialValues={{ status: true, solution_status_0: true, solution_type_0: 'text' }}
initialValues={{
status: true,
solution_status_0: true,
solution_type_0: 'text',
}}
onValuesChange={checkFirstSolutionValid}
>
<ErrorCodeForm
@@ -347,10 +405,13 @@ const AddBrandDevice = () => {
onCreateNewErrorCode={handleCreateNewErrorCode}
onResetForm={resetErrorCodeForm}
errorCodes={errorCodes}
errorCodeIcon={errorCodeIcon}
onErrorCodeIconUpload={handleErrorCodeIconUpload}
onErrorCodeIconRemove={handleErrorCodeIconRemove}
/>
</Form>
</Col>
<Col span={12}>
<Col span={16}>
<ErrorCodeTable
errorCodes={errorCodes}
loading={loading}
@@ -368,14 +429,14 @@ const AddBrandDevice = () => {
return (
<Card>
<Title level={4} style={{ margin: '0 0 24px 0' }}>Tambah Brand Device</Title>
<Title level={4} style={{ margin: '0 0 24px 0' }}>
Tambah Brand Device
</Title>
<Steps current={currentStep} style={{ marginBottom: 24 }}>
<Step title="Brand Device Details" />
<Step title="Error Codes" />
</Steps>
<div style={{ marginTop: 24 }}>
{renderStepContent()}
</div>
<div style={{ marginTop: 24 }}>{renderStepContent()}</div>
<Divider />
<FormActions
currentStep={currentStep}

View File

@@ -4,6 +4,7 @@ import { Divider, Typography, Button, Steps, Form, Row, Col, Card, Spin, Modal }
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { getBrandById, updateBrand } from '../../../api/master-brand';
import { getFileUrl } from '../../../api/file-uploads';
import BrandForm from './component/BrandForm';
import ErrorCodeForm from './component/ErrorCodeForm';
import ErrorCodeTable from './component/ListErrorCode';
@@ -37,6 +38,7 @@ const EditBrandDevice = () => {
const [loading, setLoading] = useState(true);
const [formData, setFormData] = useState(defaultData);
const [errorCodes, setErrorCodes] = useState([]);
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
const {
solutionFields,
@@ -50,7 +52,7 @@ const EditBrandDevice = () => {
handleSolutionStatusChange,
resetSolutionFields,
checkFirstSolutionValid,
setSolutionsForExistingRecord
setSolutionsForExistingRecord,
} = useErrorCodeLogic(errorCodeForm, fileList);
useEffect(() => {
@@ -61,7 +63,8 @@ const EditBrandDevice = () => {
return;
}
const savedPhase = location.state?.phase || localStorage.getItem(`brand_device_edit_${id}_last_phase`);
const savedPhase =
location.state?.phase || localStorage.getItem(`brand_device_edit_${id}_last_phase`);
if (savedPhase) {
setCurrentStep(parseInt(savedPhase));
localStorage.removeItem(`brand_device_edit_${id}_last_phase`);
@@ -70,9 +73,22 @@ const EditBrandDevice = () => {
setBreadcrumbItems([
{ title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}> Master</span> },
{
title: <span style={{ fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }} onClick={() => navigate('/master/brand-device')}>Brand Device</span>
title: (
<span
style={{ fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}
onClick={() => navigate('/master/brand-device')}
>
Brand Device
</span>
),
},
{
title: (
<span style={{ fontSize: '14px', fontWeight: 'bold' }}>
Edit Brand Device
</span>
),
},
{ title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}>Edit Brand Device</span> }
]);
try {
@@ -90,20 +106,34 @@ const EditBrandDevice = () => {
brand_code: brandData.brand_code,
};
const existingErrorCodes = brandData.error_code ? brandData.error_code.map((ec, index) => ({
key: `existing-${ec.error_code_id}`,
error_code_id: ec.error_code_id,
error_code: ec.error_code,
error_code_name: ec.error_code_name || '',
error_code_description: ec.error_code_description || '',
status: ec.is_active,
solution: ec.solution || []
})) : [];
const existingErrorCodes = brandData.error_code
? brandData.error_code.map((ec, index) => ({
key: `existing-${ec.error_code_id}`,
error_code_id: ec.error_code_id,
error_code: ec.error_code,
error_code_name: ec.error_code_name || '',
error_code_description: ec.error_code_description || '',
error_code_color: ec.error_code_color || '#000000',
path_icon: ec.path_icon || '',
status: ec.is_active,
solution: ec.solution || [],
errorCodeIcon: ec.path_icon ? {
name: 'icon',
uploadPath: ec.path_icon,
url: (() => {
const pathParts = ec.path_icon.split('/');
const folder = pathParts[0];
const filename = pathParts.slice(1).join('/');
return getFileUrl(folder, filename);
})(),
type_solution: 'image'
} : null,
}))
: [];
setFormData(newFormData);
brandForm.setFieldsValue(newFormData);
setErrorCodes(existingErrorCodes);
} else {
NotifAlert({
icon: 'error',
@@ -135,7 +165,11 @@ const EditBrandDevice = () => {
await brandForm.validateFields();
setCurrentStep(1);
} catch (error) {
NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk brand device!' });
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Harap isi semua kolom wajib untuk brand device!',
});
}
};
@@ -148,19 +182,21 @@ const EditBrandDevice = () => {
brand_model: formData.brand_model || '',
brand_manufacture: formData.brand_manufacture,
is_active: formData.is_active,
error_code: errorCodes.map(ec => ({
error_code: errorCodes.map((ec) => ({
error_code: ec.error_code,
error_code_name: ec.error_code_name || '',
error_code_description: ec.error_code_description || '',
error_code_color: ec.error_code_color || '#000000',
path_icon: ec.errorCodeIcon?.uploadPath || ec.path_icon || '',
is_active: ec.status !== undefined ? ec.status : true,
solution: (ec.solution || []).map(sol => ({
solution: (ec.solution || []).map((sol) => ({
solution_name: sol.solution_name,
type_solution: sol.type_solution,
text_solution: sol.text_solution || '',
path_solution: sol.path_solution || '',
is_active: sol.is_active !== false
}))
}))
is_active: sol.is_active !== false,
})),
})),
};
const response = await updateBrand(id, finalFormData);
@@ -182,9 +218,9 @@ const EditBrandDevice = () => {
}
} catch (error) {
NotifAlert({
icon: "error",
title: "Gagal",
message: error.message || "Gagal mengupdate data. Silakan coba lagi.",
icon: 'error',
title: 'Gagal',
message: error.message || 'Gagal mengupdate data. Silakan coba lagi.',
});
} finally {
setConfirmLoading(false);
@@ -196,8 +232,10 @@ const EditBrandDevice = () => {
error_code: record.error_code,
error_code_name: record.error_code_name,
error_code_description: record.error_code_description,
error_code_color: record.error_code_color,
status: record.status,
});
setErrorCodeIcon(record.errorCodeIcon || null);
setIsErrorCodeFormReadOnly(true);
setEditingErrorCodeKey(record.key);
@@ -211,8 +249,10 @@ const EditBrandDevice = () => {
error_code: record.error_code,
error_code_name: record.error_code_name,
error_code_description: record.error_code_description,
error_code_color: record.error_code_color,
status: record.status,
});
setErrorCodeIcon(record.errorCodeIcon || null);
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(record.key);
@@ -226,22 +266,29 @@ const EditBrandDevice = () => {
}
};
const handleAddErrorCode = (newErrorCode) => {
// Include the current icon in the error code
const errorCodeWithIcon = {
...newErrorCode,
errorCodeIcon: errorCodeIcon
};
let updatedErrorCodes;
if (editingErrorCodeKey) {
updatedErrorCodes = errorCodes.map(item => item.key === editingErrorCodeKey ? newErrorCode : item);
updatedErrorCodes = errorCodes.map((item) =>
item.key === editingErrorCodeKey ? errorCodeWithIcon : item
);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil diupdate!'
message: 'Error code berhasil diupdate!',
});
} else {
updatedErrorCodes = [...errorCodes, newErrorCode];
updatedErrorCodes = [...errorCodes, errorCodeWithIcon];
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil ditambahkan!'
message: 'Error code berhasil ditambahkan!',
});
}
@@ -254,9 +301,10 @@ const EditBrandDevice = () => {
errorCodeForm.setFieldsValue({
status: true,
solution_status_0: true,
solution_type_0: 'text'
solution_type_0: 'text',
});
setFileList([]);
setErrorCodeIcon(null);
resetSolutionFields();
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(null);
@@ -267,17 +315,17 @@ const EditBrandDevice = () => {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap brand harus memiliki minimal 1 error code!'
message: 'Setiap brand harus memiliki minimal 1 error code!',
});
return;
}
const updatedErrorCodes = errorCodes.filter(item => item.key !== key);
const updatedErrorCodes = errorCodes.filter((item) => item.key !== key);
setErrorCodes(updatedErrorCodes);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil dihapus!'
message: 'Error code berhasil dihapus!',
});
};
@@ -285,6 +333,14 @@ const EditBrandDevice = () => {
resetErrorCodeForm();
};
const handleErrorCodeIconUpload = (iconData) => {
setErrorCodeIcon(iconData);
};
const handleErrorCodeIconRemove = () => {
setErrorCodeIcon(null);
};
const handleFileView = (pathSolution, fileType) => {
localStorage.setItem(`brand_device_edit_${id}_last_phase`, currentStep.toString());
@@ -297,7 +353,7 @@ const EditBrandDevice = () => {
editingErrorCodeKey: editingErrorCodeKey,
isErrorCodeFormReadOnly: isErrorCodeFormReadOnly,
solutionsToDelete: Array.from(solutionsToDelete),
currentSolutionData: window.currentSolutionData || {}
currentSolutionData: window.currentSolutionData || {},
};
localStorage.setItem(`brand_device_edit_${id}_temp_data`, JSON.stringify(tempData));
@@ -314,11 +370,11 @@ const EditBrandDevice = () => {
};
const handleSolutionFileUpload = (file) => {
setFileList(prevList => [...prevList, file]);
setFileList((prevList) => [...prevList, file]);
};
const handleFileRemove = (file) => {
const newFileList = fileList.filter(item => item.uid !== file.uid);
const newFileList = fileList.filter((item) => item.uid !== file.uid);
setFileList(newFileList);
};
@@ -328,7 +384,9 @@ const EditBrandDevice = () => {
<BrandForm
form={brandForm}
formData={formData}
onValuesChange={(changedValues, allValues) => setFormData(prev => ({...prev, ...allValues}))}
onValuesChange={(changedValues, allValues) =>
setFormData((prev) => ({ ...prev, ...allValues }))
}
isEdit={true}
/>
);
@@ -337,17 +395,24 @@ const EditBrandDevice = () => {
if (currentStep === 1) {
return (
<Row gutter={24}>
<Col span={12}>
<Col span={8}>
<Title level={5} style={{ marginBottom: 16 }}>
{isErrorCodeFormReadOnly
? (editingErrorCodeKey ? 'View Error Code' : 'Error Code Form')
: (editingErrorCodeKey ? 'Edit Error Code' : 'Tambah Error Code')
}
? editingErrorCodeKey
? 'View Error Code'
: 'Error Code Form'
: editingErrorCodeKey
? 'Edit Error Code'
: 'Tambah Error Code'}
</Title>
<Form
form={errorCodeForm}
layout="vertical"
initialValues={{ status: true, solution_status_0: true, solution_type_0: 'text' }}
initialValues={{
status: true,
solution_status_0: true,
solution_type_0: 'text',
}}
onValuesChange={checkFirstSolutionValid}
>
<ErrorCodeForm
@@ -370,19 +435,23 @@ const EditBrandDevice = () => {
onCreateNewErrorCode={handleCreateNewErrorCode}
onResetForm={resetErrorCodeForm}
errorCodes={errorCodes}
errorCodeIcon={errorCodeIcon}
onErrorCodeIconUpload={handleErrorCodeIconUpload}
onErrorCodeIconRemove={handleErrorCodeIconRemove}
/>
</Form>
</Col>
<Col span={12}>
<Col span={16}>
<ErrorCodeTable
errorCodes={loading ?
Array.from({ length: 3 }, (_, index) => ({
key: `loading-${index}`,
error_code: 'Loading...',
error_code_name: 'Loading...',
solution: []
})) :
errorCodes
errorCodes={
loading
? Array.from({ length: 3 }, (_, index) => ({
key: `loading-${index}`,
error_code: 'Loading...',
error_code_name: 'Loading...',
solution: [],
}))
: errorCodes
}
loading={loading}
onPreview={handlePreviewErrorCode}
@@ -399,32 +468,41 @@ const EditBrandDevice = () => {
return (
<Card>
<Title level={4} style={{ margin: '0 0 24px 0' }}>Edit Brand Device</Title>
<Title level={4} style={{ margin: '0 0 24px 0' }}>
Edit Brand Device
</Title>
<Steps current={currentStep} style={{ marginBottom: 24 }}>
<Step title="Brand Device Details" />
<Step title="Error Codes" />
</Steps>
<div style={{ position: 'relative' }}>
{loading && (
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.6)',
backdropFilter: 'blur(0.8px)',
filter: 'blur(0.5px)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
borderRadius: '8px'
}}>
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.6)',
backdropFilter: 'blur(0.8px)',
filter: 'blur(0.5px)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
borderRadius: '8px',
}}
>
<Spin size="large" />
</div>
)}
<div style={{ filter: loading ? 'blur(0.5px)' : 'none', transition: 'filter 0.3s ease' }}>
<div
style={{
filter: loading ? 'blur(0.5px)' : 'none',
transition: 'filter 0.3s ease',
}}
>
{renderStepContent()}
</div>
</div>

View File

@@ -1,11 +1,10 @@
import React, { memo, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import { Typography, Card, Row, Col, Tag, Button, Space, Descriptions, Divider, Table, Steps, Collapse, Switch, Skeleton, Spin, Modal } from 'antd';
import { ArrowLeftOutlined, EditOutlined, DeleteOutlined, FileTextOutlined, FilePdfOutlined, EyeOutlined } from '@ant-design/icons';
import { Typography, Card, Row, Col, Tag, Button, Space, Descriptions, Divider, Steps, Collapse, Switch, Spin, Modal, Empty } from 'antd';
import { ArrowLeftOutlined, FileTextOutlined, FilePdfOutlined, EyeOutlined } from '@ant-design/icons';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { NotifConfirmDialog, NotifOk, NotifAlert } from '../../../components/Global/ToastNotif';
import { getBrandById, deleteBrand } from '../../../api/master-brand';
import TableList from '../../../components/Global/TableList';
import { NotifOk, NotifAlert } from '../../../components/Global/ToastNotif';
import { getBrandById } from '../../../api/master-brand';
const { Title, Text } = Typography;
const { Step } = Steps;
@@ -19,8 +18,7 @@ const ViewBrandDevice = () => {
const [brandData, setBrandData] = useState(null);
const [loading, setLoading] = useState(true);
const [currentStep, setCurrentStep] = useState(0);
const [errorCodesTriger, setErrorCodesTriger] = useState(0);
const [activeErrorKeys, setActiveErrorKeys] = useState([]);
useEffect(() => {
const fetchBrandData = async () => {
@@ -44,10 +42,8 @@ const ViewBrandDevice = () => {
setLoading(true);
const response = await getBrandById(id);
if (response && response.statusCode === 200) {
setBrandData(response.data);
setErrorCodesTriger(prev => prev + 1);
} else {
NotifAlert({
icon: 'error',
@@ -73,264 +69,21 @@ const ViewBrandDevice = () => {
fetchBrandData();
}, [id, setBreadcrumbItems, navigate, location.state]);
// const handleEdit = () => {
// navigate(`/master/brand-device/edit/${id}`);
// };
// const handleDelete = () => {
// NotifConfirmDialog({
// icon: 'question',
// title: 'Konfirmasi Hapus',
// message: `Brand Device "${brandData?.brand_name}" akan dihapus?`,
// onConfirm: async () => {
// try {
// const response = await deleteBrand(id);
// if (response && response.statusCode === 200) {
// NotifOk({
// icon: 'success',
// title: 'Berhasil',
// message: response.message || 'Brand Device berhasil dihapus.',
// });
// navigate('/master/brand-device');
// } else {
// NotifAlert({
// icon: 'error',
// title: 'Gagal',
// message: response?.message || 'Gagal menghapus Brand Device',
// });
// }
// } catch (error) {
// console.error('Delete Brand Device Error:', error);
// NotifAlert({
// icon: 'error',
// title: 'Error',
// message: error.message || 'Gagal menghapus Brand Device',
// });
// }
// },
// onCancel: () => {},
// });
// };
// Fungsi untuk membuka file viewer di halaman baru
const handleFileView = (fileName, fileType) => {
console.log('handleFileView called with:', { fileName, fileType });
// Save current phase before navigating to file viewer
localStorage.setItem(`brand_device_${id}_last_phase`, currentStep.toString());
// Extract only the filename without folder prefix
let actualFileName = fileName;
if (fileName && fileName.includes('/')) {
const parts = fileName.split('/');
actualFileName = parts[parts.length - 1]; // Get the last part (actual filename)
actualFileName = parts[parts.length - 1];
}
console.log('Processed filename:', { original: fileName, actual: actualFileName });
const encodedFileName = encodeURIComponent(actualFileName);
const fileTypeParam = fileType === 'image' ? 'image' : 'pdf';
const navigationPath = `/master/brand-device/view/${id}/files/${fileTypeParam}/${encodedFileName}`;
console.log('Navigating to:', navigationPath);
navigate(navigationPath);
};
// if (loading) {
// return (
// <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
// <Spin size="large" />
// </div>
// );
// }
if (!brandData && !loading) {
return <div>Brand Device not found</div>;
}
// Error code table columns configuration
const errorCodeColumns = [
{
title: 'No',
key: 'no',
width: '5%',
align: 'center',
render: (_, __, index) => index + 1,
},
{
title: 'Error Code',
dataIndex: 'error_code',
key: 'error_code',
width: '15%',
render: (text) => text || '-',
},
{
title: 'Error Code Name',
dataIndex: 'error_code_name',
key: 'error_code_name',
width: '20%',
render: (text) => text || '-',
},
{
title: 'Description',
dataIndex: 'error_code_description',
key: 'error_code_description',
width: '25%',
render: (text) => text || '-',
},
{
title: 'Solutions',
dataIndex: 'solution',
key: 'solution',
width: '20%',
render: (solutions) => (
<div>
{solutions && solutions.length > 0 ? (
<div>
<Text type="secondary">{solutions.length} solution(s)</Text>
<div style={{ marginTop: 4 }}>
{solutions.slice(0, 2).map((sol, index) => (
<div key={index} style={{ fontSize: '12px', color: '#666' }}>
{sol.type_solution === 'text' ? (
<span> {sol.solution_name}</span>
) : (
<span> {sol.solution_name} ({sol.type_solution})</span>
)}
</div>
))}
{solutions.length > 2 && (
<div style={{ fontSize: '12px', color: '#999' }}>
...and {solutions.length - 2} more
</div>
)}
</div>
</div>
) : (
<Text type="secondary">No solutions</Text>
)}
</div>
)
},
{
title: 'Status',
dataIndex: 'is_active',
key: 'is_active',
width: '10%',
align: 'center',
render: (_, { is_active }) => (
<Tag color={is_active ? 'green' : 'red'}>
{is_active ? 'Active' : 'Inactive'}
</Tag>
),
},
{
title: 'Action',
key: 'action',
align: 'center',
width: '5%',
render: (_, record) => (
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => {
// Show detailed view for this error code
Modal.info({
title: 'Error Code Details',
width: 800,
content: (
<div style={{ marginTop: 16 }}>
<Descriptions bordered column={1} size="small">
<Descriptions.Item label="Error Code">{record.error_code}</Descriptions.Item>
<Descriptions.Item label="Error Code Name">{record.error_code_name}</Descriptions.Item>
<Descriptions.Item label="Description">{record.error_code_description}</Descriptions.Item>
<Descriptions.Item label="Status">
<Tag color={record.is_active ? 'green' : 'red'}>
{record.is_active ? 'Active' : 'Inactive'}
</Tag>
</Descriptions.Item>
</Descriptions>
<Title level={5} style={{ marginTop: 24, marginBottom: 16 }}>
Solutions ({record.solution?.length || 0})
</Title>
{record.solution && record.solution.length > 0 ? (
<Row gutter={[16, 16]}>
{record.solution.map((solution) => (
<Col span={24} key={solution.brand_code_solution_id}>
<Card size="small">
<Row justify="space-between" align="middle">
<Col>
<Space>
{solution.type_solution === 'pdf' ? (
<FilePdfOutlined style={{ color: '#ff4d4f', fontSize: '16px' }} />
) : (
<FileTextOutlined style={{ color: '#1890ff', fontSize: '16px' }} />
)}
<Text strong>{solution.solution_name}</Text>
</Space>
</Col>
<Col>
<Tag color={solution.type_solution === 'pdf' ? 'red' : 'blue'}>
{solution.type_solution.toUpperCase()}
</Tag>
</Col>
</Row>
<div style={{ marginTop: 12 }}>
{solution.type_solution === 'text' ? (
<Text>{solution.text_solution}</Text>
) : (
<Space>
<Text type="secondary">File: {solution.path_document || solution.path_solution}</Text>
{solution.path_document && (
<Button
type="link"
size="small"
onClick={() => {
handleFileView(
(solution.path_document || solution.path_solution || solution.file_upload_name || solution.solution_name || 'Document')?.toString(),
solution.type_solution || 'pdf'
);
}}
>
View Document
</Button>
)}
</Space>
)}
</div>
</Card>
</Col>
))}
</Row>
) : (
<Text type="secondary">No solutions available</Text>
)}
</div>
),
});
}}
style={{ color: '#1890ff' }}
/>
),
},
];
// Mock data function for error codes
const getErrorCodesData = async () => {
const errorCodes = brandData?.error_code || [];
return {
data: errorCodes,
paging: {
current_page: 1,
current_limit: 10,
total_limit: errorCodes.length,
total_page: 1,
}
};
};
const renderStepContent = () => {
if (currentStep === 0) {
return (
@@ -444,26 +197,140 @@ const ViewBrandDevice = () => {
}
if (currentStep === 1) {
const errorCodesCount = loading ? 3 : (brandData?.error_code?.length || 0);
const errorCodes = brandData?.error_code || [];
return (
<div>
<Title level={5} style={{ marginBottom: 16 }}>
Error Codes ({errorCodesCount})
Error Codes ({errorCodes.length})
</Title>
{errorCodesCount > 0 ? (
<TableList
mobile={false}
cardColor={'#42AAFF'}
header={'error_code'}
getData={getErrorCodesData}
queryParams={{}}
columns={errorCodeColumns}
triger={errorCodesTriger}
firstLoad={false}
/>
{errorCodes.length > 0 ? (
<Collapse
activeKey={activeErrorKeys}
onChange={setActiveErrorKeys}
style={{ marginBottom: 16 }}
>
{errorCodes.map((errorCode, index) => (
<Panel
key={index}
header={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
<div>
<Text strong style={{ fontSize: '14px' }}>{errorCode.error_code}</Text>
<Text style={{ marginLeft: 8, fontSize: '12px', color: '#666' }}>
- {errorCode.error_code_name}
</Text>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Tag color={errorCode.is_active ? 'green' : 'red'} style={{ margin: 0 }}>
{errorCode.is_active ? 'Active' : 'Inactive'}
</Tag>
<Text style={{ fontSize: '12px', color: '#999' }}>
{errorCode.solution?.length || 0} solution(s)
</Text>
</div>
</div>
}
>
<div style={{ padding: '12px 0' }}>
<div style={{ marginBottom: 16 }}>
<Text type="secondary">Description:</Text>
<div style={{ marginTop: 4 }}>
<Text>{errorCode.error_code_description || 'No description'}</Text>
</div>
</div>
<div>
<Text strong>Solutions:</Text>
{errorCode.solution && errorCode.solution.length > 0 ? (
<div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 12 }}>
{errorCode.solution.map((solution) => (
<Card
key={solution.brand_code_solution_id}
size="small"
style={{
backgroundColor: '#fafafa',
border: '1px solid #f0f0f0'
}}
>
<Row justify="space-between" align="middle">
<Col>
<Space>
{solution.type_solution === 'pdf' ? (
<FilePdfOutlined style={{ color: '#ff4d4f', fontSize: '16px' }} />
) : solution.type_solution === 'image' ? (
<EyeOutlined style={{ color: '#1890ff', fontSize: '16px' }} />
) : (
<FileTextOutlined style={{ color: '#1890ff', fontSize: '16px' }} />
)}
<Text strong style={{ fontSize: '13px' }}>{solution.solution_name}</Text>
</Space>
</Col>
<Col>
<Tag
color={
solution.type_solution === 'pdf' ? 'red' :
solution.type_solution === 'image' ? 'blue' :
'default'
}
style={{ fontSize: '11px' }}
>
{solution.type_solution ? solution.type_solution.toUpperCase() : 'TEXT'}
</Tag>
</Col>
</Row>
<div style={{ marginTop: 8 }}>
{solution.type_solution === 'text' ? (
<Text style={{ fontSize: '12px', color: '#666' }}>
{solution.text_solution}
</Text>
) : (
<div>
<Text style={{ fontSize: '12px', color: '#666' }}>
File: {solution.path_document || solution.path_solution || 'Document'}
</Text>
{(solution.path_document || solution.path_solution) && (
<Button
type="link"
size="small"
onClick={() => {
handleFileView(
(solution.path_document || solution.path_solution || solution.file_upload_name || solution.solution_name || 'Document')?.toString(),
solution.type_solution || 'pdf'
);
}}
style={{ padding: 0, height: 'auto', fontSize: '12px', marginLeft: 8 }}
>
View
</Button>
)}
</div>
)}
</div>
</Card>
))}
</div>
) : (
<div style={{ marginTop: 12 }}>
<Text type="secondary" style={{ fontSize: '12px' }}>No solutions available</Text>
</div>
)}
</div>
</div>
</Panel>
))}
</Collapse>
) : (
!loading && <Text type="secondary">No error codes available</Text>
!loading && (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
<Text type="secondary">No error codes available</Text>
}
/>
)
)}
</div>
);
@@ -483,7 +350,7 @@ const ViewBrandDevice = () => {
<Space>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/master/brand-device')}
onClick={() => navigate('/master/brand-device')}
>
Kembali
</Button>
@@ -496,9 +363,7 @@ const ViewBrandDevice = () => {
<Step title="Error Codes & Solutions" />
</Steps>
{/* Content area with blur overlay during loading */}
<div style={{ position: 'relative', marginTop: 24 }}>
{/* Overlay with blur effect during loading - only on content area */}
{loading && (
<div style={{
position: 'absolute',
@@ -544,7 +409,7 @@ const ViewBrandDevice = () => {
)}
</div>
</Card>
</React.Fragment>
</React.Fragment>
);
};

View File

@@ -94,13 +94,15 @@ const ViewFilePage = () => {
setPdfLoading(true);
const folder = getFolderFromFileType('pdf');
try {
const response = await getFile(folder, decodedFileName);
const blobUrl = window.URL.createObjectURL(response.data);
const blobData = await getFile(folder, decodedFileName);
console.log('PDF blob data received:', blobData);
const blobUrl = window.URL.createObjectURL(blobData);
setPdfBlobUrl(blobUrl);
console.log('PDF blob URL created successfully:', blobUrl);
} catch (pdfError) {
console.error('Error loading PDF:', pdfError);
setError('Failed to load PDF file');
setError('Failed to load PDF file: ' + (pdfError.message || pdfError));
setPdfBlobUrl(null);
} finally {
setPdfLoading(false);
}
@@ -194,7 +196,7 @@ const ViewFilePage = () => {
const isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension);
const isPdf = fileExtension === 'pdf';
const fileUrl = loading ? null : getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName);
// const fileUrl = loading ? null : getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName);
// Show placeholder when loading
if (loading) {
@@ -260,7 +262,7 @@ const ViewFilePage = () => {
return (
<div style={{ textAlign: 'center', padding: '20px' }}>
<img
src={fileUrl}
src={getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName)}
alt={actualFileName}
style={{
maxWidth: '100%',
@@ -276,7 +278,7 @@ const ViewFilePage = () => {
}
if (isPdf) {
const displayUrl = pdfBlobUrl || fileUrl;
const displayUrl = pdfBlobUrl || getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName);
return (
<div style={{ height: '75vh', width: '100%', border: '1px solid #d9d9d9', borderRadius: '8px', overflow: 'hidden' }}>
@@ -342,13 +344,15 @@ const ViewFilePage = () => {
setPdfLoading(true);
const folder = getFolderFromFileType('pdf');
getFile(folder, actualFileName)
.then(response => {
const blobUrl = window.URL.createObjectURL(response.data);
.then(blobData => {
console.log('Retry PDF blob data:', blobData);
const blobUrl = window.URL.createObjectURL(blobData);
setPdfBlobUrl(blobUrl);
})
.catch(error => {
console.error('Error retrying PDF load:', error);
setError('Failed to load PDF file');
setError('Failed to load PDF file: ' + (error.message || error));
setPdfBlobUrl(null);
})
.finally(() => {
setPdfLoading(false);
@@ -370,7 +374,7 @@ const ViewFilePage = () => {
<div style={{ fontSize: '16px', marginBottom: '8px' }}>Preview tidak tersedia untuk jenis file ini</div>
<div style={{ color: '#666', marginBottom: '16px' }}>{actualFileName}</div>
<div style={{ marginTop: '16px' }}>
<Button type="primary" href={fileUrl} target="_blank" rel="noopener noreferrer">
<Button type="primary" href={getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName)} target="_blank" rel="noopener noreferrer">
Buka di Tab Baru
</Button>
</div>

View File

@@ -1,7 +1,18 @@
import { Form, Divider, Button, Switch, Input, ConfigProvider, Typography } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import {
Form,
Divider,
Button,
Switch,
Input,
ConfigProvider,
Typography,
Upload,
message,
} from 'antd';
import { PlusOutlined, UploadOutlined, DeleteOutlined } from '@ant-design/icons';
import { NotifAlert } from '../../../../components/Global/ToastNotif';
import SolutionField from './SolutionField';
import { uploadFile, getFileUrl } from '../../../../api/file-uploads';
const { Text } = Typography;
@@ -24,10 +35,70 @@ const ErrorCodeForm = ({
onFileView,
onCreateNewErrorCode,
onResetForm,
errorCodes
errorCodes,
errorCodeIcon,
onErrorCodeIconUpload,
onErrorCodeIconRemove,
}) => {
const statusValue = Form.useWatch('status', errorCodeForm);
const handleIconUpload = async (file) => {
// Check if file is an image
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('You can only upload image files!');
return Upload.LIST_IGNORE;
}
// Check file size (max 2MB)
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('Image must be smaller than 2MB!');
return Upload.LIST_IGNORE;
}
try {
const fileExtension = file.name.split('.').pop().toLowerCase();
const isImageFile = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(
fileExtension
);
const fileType = isImageFile ? 'image' : 'pdf';
const folder = 'images';
const uploadResponse = await uploadFile(file, folder);
const iconPath =
uploadResponse.data?.path_icon || uploadResponse.data?.path_solution || '';
if (iconPath) {
// Extract folder and filename from the path
const pathParts = iconPath.split('/');
const folder = pathParts[0];
const filename = pathParts.slice(1).join('/');
onErrorCodeIconUpload({
name: file.name,
uploadPath: iconPath,
url: getFileUrl(folder, filename), // Use the same endpoint as file uploads
type_solution: fileType,
solutionId: 'icon',
});
message.success(`${file.name} uploaded successfully!`);
} else {
message.error('Failed to upload icon');
}
} catch (error) {
console.error('Error uploading icon:', error);
message.error('Failed to upload icon');
}
return false; // Prevent default upload behavior
};
const handleRemoveIcon = () => {
onErrorCodeIconRemove();
message.success('Icon removed');
};
const handleAddErrorCode = async () => {
try {
const values = await errorCodeForm.validateFields();
@@ -41,7 +112,7 @@ const ErrorCodeForm = ({
const solutionName = values[`solution_name_${fieldId}`];
const textSolution = values[`text_solution_${fieldId}`];
const filesForSolution = fileList.filter(file => file.solutionId === fieldId);
const filesForSolution = fileList.filter((file) => file.solutionId === fieldId);
const solutionType = values[`solution_type_${fieldId}`] || solutionTypes[fieldId];
if (solutionType === 'text') {
@@ -51,11 +122,12 @@ const ErrorCodeForm = ({
type_solution: 'text',
text_solution: textSolution.trim(),
path_solution: '',
is_active: solutionStatuses[fieldId] !== false
is_active: solutionStatuses[fieldId] !== false,
};
if (window.currentSolutionData && window.currentSolutionData[fieldId]) {
solutionData.brand_code_solution_id = window.currentSolutionData[fieldId].brand_code_solution_id;
solutionData.brand_code_solution_id =
window.currentSolutionData[fieldId].brand_code_solution_id;
}
solutions.push(solutionData);
@@ -63,15 +135,22 @@ const ErrorCodeForm = ({
} else if (solutionType === 'file') {
filesForSolution.forEach((file) => {
const solutionData = {
solution_name: solutionName || file.solution_name || file.name || `Solution ${fieldId}`,
type_solution: file.type_solution || (file.type.startsWith('image/') ? 'image' : 'pdf'),
solution_name:
solutionName ||
file.solution_name ||
file.name ||
`Solution ${fieldId}`,
type_solution:
file.type_solution ||
(file.type.startsWith('image/') ? 'image' : 'pdf'),
text_solution: '',
path_solution: file.uploadPath,
is_active: solutionStatuses[fieldId] !== false
is_active: solutionStatuses[fieldId] !== false,
};
if (window.currentSolutionData && window.currentSolutionData[fieldId]) {
solutionData.brand_code_solution_id = window.currentSolutionData[fieldId].brand_code_solution_id;
solutionData.brand_code_solution_id =
window.currentSolutionData[fieldId].brand_code_solution_id;
}
solutions.push(solutionData);
@@ -83,7 +162,8 @@ const ErrorCodeForm = ({
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap error code harus memiliki minimal 1 solution (text atau file)!'
message:
'Setiap error code harus memiliki minimal 1 solution (text atau file)!',
});
return;
}
@@ -92,15 +172,20 @@ const ErrorCodeForm = ({
error_code: values.error_code,
error_code_name: values.error_code_name,
error_code_description: values.error_code_description,
error_code_color: values.error_code_color || '#000000',
path_icon: errorCodeIcon?.uploadPath || '',
status: values.status === undefined ? true : values.status,
solution: solutions,
key: editingErrorCodeKey || `temp-${Date.now()}`
key: editingErrorCodeKey || `temp-${Date.now()}`,
};
onAddErrorCode(newErrorCode);
} catch (error) {
NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib (error code + minimal 1 solution)!' });
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Harap isi semua kolom wajib (error code + minimal 1 solution)!',
});
}
};
@@ -109,14 +194,21 @@ const ErrorCodeForm = ({
errorCodeForm.setFieldsValue({
status: true,
solution_status_0: true,
solution_type_0: 'text'
solution_type_0: 'text',
});
onResetForm();
};
return (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
}}
>
<Form.Item label="Status" style={{ margin: 0 }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Form.Item name="status" valuePropName="checked" noStyle>
@@ -143,25 +235,110 @@ const ErrorCodeForm = ({
},
}}
>
<Button
icon={<PlusOutlined />}
onClick={handleAddErrorCode}
>
<Button icon={<PlusOutlined />} onClick={handleAddErrorCode}>
{editingErrorCodeKey ? 'Update Error Code' : 'Tambah Error Code'}
</Button>
</ConfigProvider>
)}
</div>
<Form.Item name="error_code" label="Error Code" rules={[{ required: true, message: 'Error Code wajib diisi' }]}>
<Form.Item
name="error_code"
label="Error Code"
rules={[{ required: true, message: 'Error Code wajib diisi' }]}
>
<Input disabled={isErrorCodeFormReadOnly} />
</Form.Item>
<Form.Item name="error_code_name" label="Error Code Name" rules={[{ required: true, message: 'Error Code Name wajib diisi' }]}>
<Form.Item
name="error_code_name"
label="Error Code Name"
rules={[{ required: true, message: 'Error Code Name wajib diisi' }]}
>
<Input disabled={isErrorCodeFormReadOnly} />
</Form.Item>
<Form.Item name="error_code_description" label="Error Code Description" rules={[{ required: true, message: 'Error Code Description wajib diisi' }]}>
<Form.Item label="Color & Icon">
<div style={{ display: 'flex', alignItems: 'center', gap: 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, minWidth: 40 }}>Icon:</Text>
{errorCodeIcon ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<img
src={errorCodeIcon.url}
alt="Error Code Icon"
style={{
width: 32,
height: 32,
objectFit: 'cover',
border: '1px solid #d9d9d9',
borderRadius: 4,
}}
/>
<div>
<div style={{ fontSize: 11, color: '#666', marginBottom: 2 }}>
{errorCodeIcon.name.length > 15
? errorCodeIcon.name.substring(0, 15) + '...'
: errorCodeIcon.name}
</div>
{!isErrorCodeFormReadOnly && (
<Button
size="small"
type="text"
danger
icon={<DeleteOutlined />}
onClick={handleRemoveIcon}
style={{ height: 20, padding: '0 4px', fontSize: 10 }}
>
Remove
</Button>
)}
</div>
</div>
) : (
<Upload
accept="image/*"
beforeUpload={handleIconUpload}
showUploadList={false}
disabled={isErrorCodeFormReadOnly}
>
<Button
size="small"
icon={<UploadOutlined />}
disabled={isErrorCodeFormReadOnly}
style={{ height: 32 }}
>
Upload
</Button>
</Upload>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, minWidth: 40 }}>Color:</Text>
<Form.Item name="error_code_color" noStyle>
<Input
type="color"
disabled={isErrorCodeFormReadOnly}
style={{
width: 50,
height: 32,
border: '1px solid #d9d9d9',
borderRadius: 4,
}}
/>
</Form.Item>
</div>
</div>
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
Choose color and upload icon (max 2MB, JPG/PNG/GIF)
</div>
</Form.Item>
<Form.Item
name="error_code_description"
label="Error Code Description"
rules={[{ required: true, message: 'Error Code Description wajib diisi' }]}
>
<Input.TextArea disabled={isErrorCodeFormReadOnly} />
</Form.Item>
@@ -175,7 +352,7 @@ const ErrorCodeForm = ({
solutionType={solutionTypes[fieldId]}
solutionStatus={solutionStatuses[fieldId]}
isReadOnly={isErrorCodeFormReadOnly}
fileList={fileList.filter(file => file.solutionId === fieldId)}
fileList={fileList.filter((file) => file.solutionId === fieldId)}
onRemove={() => onRemoveSolutionField(fieldId)}
onSolutionTypeChange={(type) => onSolutionTypeChange(fieldId, type)}
onSolutionStatusChange={(status) => onSolutionStatusChange(fieldId, status)}
@@ -200,17 +377,13 @@ const ErrorCodeForm = ({
{!isErrorCodeFormReadOnly && editingErrorCodeKey && (
<Form.Item style={{ textAlign: 'right', marginTop: 16 }}>
<Button onClick={handleResetForm}>
Kembali
</Button>
<Button onClick={handleResetForm}>Kembali</Button>
</Form.Item>
)}
{isErrorCodeFormReadOnly && editingErrorCodeKey && (
<Form.Item style={{ textAlign: 'right', marginTop: 16 }}>
<Button onClick={handleResetForm}>
Kembali
</Button>
<Button onClick={handleResetForm}>Kembali</Button>
</Form.Item>
)}
</>

View File

@@ -18,36 +18,53 @@ const SolutionField = ({
onFileUpload,
currentSolutionData,
onFileView,
errorCodeForm
errorCodeForm,
}) => {
useEffect(() => {
if (currentSolutionData && errorCodeForm) {
if (currentSolutionData.solution_name) {
errorCodeForm.setFieldValue(`solution_name_${fieldId}`, currentSolutionData.solution_name);
errorCodeForm.setFieldValue(
`solution_name_${fieldId}`,
currentSolutionData.solution_name
);
}
if (currentSolutionData.type_solution === 'text' && currentSolutionData.text_solution) {
errorCodeForm.setFieldValue(`text_solution_${fieldId}`, currentSolutionData.text_solution);
errorCodeForm.setFieldValue(
`text_solution_${fieldId}`,
currentSolutionData.text_solution
);
}
if (currentSolutionData.type_solution) {
const formValue = currentSolutionData.type_solution === 'image' || currentSolutionData.type_solution === 'pdf' ? 'file' : currentSolutionData.type_solution;
const formValue =
currentSolutionData.type_solution === 'image' ||
currentSolutionData.type_solution === 'pdf'
? 'file'
: currentSolutionData.type_solution;
errorCodeForm.setFieldValue(`solution_type_${fieldId}`, formValue);
}
if (currentSolutionData.is_active !== undefined) {
errorCodeForm.setFieldValue(`solution_status_${fieldId}`, currentSolutionData.is_active);
// Only set status if it's not already set to prevent overwriting user changes
const currentStatus = errorCodeForm.getFieldValue(`solution_status_${fieldId}`);
if (currentSolutionData.is_active !== undefined && currentStatus === undefined) {
errorCodeForm.setFieldValue(
`solution_status_${fieldId}`,
currentSolutionData.is_active
);
}
}
}, [currentSolutionData, fieldId, errorCodeForm]);
const handleBeforeUpload = async (file) => {
const isAllowedType = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif'].includes(file.type);
const isAllowedType = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif'].includes(
file.type
);
if (!isAllowedType) {
NotifAlert({
icon: 'error',
title: 'Error',
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`,
});
return Upload.LIST_IGNORE;
}
@@ -72,13 +89,13 @@ const SolutionField = ({
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `${file.name} berhasil diupload!`
message: `${file.name} berhasil diupload!`,
});
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: `Gagal mengupload ${file.name}`
message: `Gagal mengupload ${file.name}`,
});
}
} catch (error) {
@@ -86,7 +103,7 @@ const SolutionField = ({
NotifAlert({
icon: 'error',
title: 'Error',
message: `Gagal mengupload ${file.name}. Silakan coba lagi.`
message: `Gagal mengupload ${file.name}. Silakan coba lagi.`,
});
}
@@ -101,10 +118,17 @@ const SolutionField = ({
padding: 16,
border: '1px solid #d9d9d9',
borderRadius: 8,
transition: 'all 0.3s ease'
transition: 'all 0.3s ease',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
}}
>
<Text strong>Solution {index + 1}</Text>
<Button
type="text"
@@ -122,19 +146,34 @@ const SolutionField = ({
</Form.Item>
<Form.Item label="Status">
<div style={{ display: 'flex', alignItems: 'center' }}>
<Switch
checked={solutionStatus}
onChange={(checked) => {
onSolutionStatusChange(fieldId, checked);
}}
disabled={isReadOnly}
style={{ backgroundColor: solutionStatus ? '#23A55A' : '#bfbfbf' }}
/>
<Text style={{ marginLeft: 8 }}>
{solutionStatus ? 'Active' : 'Non Active'}
</Text>
</div>
<Form.Item
shouldUpdate={(prevValues, currentValues) =>
prevValues[`solution_status_${fieldId}`] !==
currentValues[`solution_status_${fieldId}`]
}
noStyle
>
{({ getFieldValue, setFieldValue }) => {
const currentStatus = getFieldValue(`solution_status_${fieldId}`);
return (
<div style={{ display: 'flex', alignItems: 'center' }}>
<Switch
checked={currentStatus === true}
onChange={(checked) => {
setFieldValue(`solution_status_${fieldId}`, checked);
}}
disabled={isReadOnly}
style={{
backgroundColor: solutionStatus ? '#23A55A' : '#bfbfbf',
}}
/>
<Text style={{ marginLeft: 8 }}>
{currentStatus === true ? 'Active' : 'Non Active'}
</Text>
</div>
);
}}
</Form.Item>
</Form.Item>
<Form.Item label="Solution Type">
@@ -151,83 +190,129 @@ const SolutionField = ({
</Form.Item>
</Form.Item>
<Form.Item shouldUpdate={(prevValues, currentValues) => prevValues[`solution_type_${fieldId}`] !== currentValues[`solution_type_${fieldId}`]} noStyle>
<Form.Item
shouldUpdate={(prevValues, currentValues) =>
prevValues[`solution_type_${fieldId}`] !==
currentValues[`solution_type_${fieldId}`]
}
noStyle
>
{({ getFieldValue }) => {
const currentType = getFieldValue(`solution_type_${fieldId}`) || 'text';
const displayType = currentType === 'file' && currentSolutionData ?
(currentSolutionData.type_solution === 'image' ? 'image' :
currentSolutionData.type_solution === 'pdf' ? 'pdf' : 'file') : currentType;
const displayType =
currentType === 'file' && currentSolutionData
? currentSolutionData.type_solution === 'image'
? 'image'
: currentSolutionData.type_solution === 'pdf'
? 'pdf'
: 'file'
: currentType;
return displayType === 'text' ? (
<Form.Item name={`text_solution_${fieldId}`} label="Text Solution">
<Input.TextArea
placeholder="Enter text solution"
disabled={isReadOnly}
rows={4}
/>
</Form.Item>
) : (
<>
{/* Show existing file info for both preview and edit mode */}
{currentSolutionData && currentSolutionData.type_solution !== 'text' && currentSolutionData.path_solution && (
<Form.Item label="Current Document">
{(() => {
const solution = currentSolutionData;
const fileName = solution.file_upload_name || solution.path_solution?.split('/')[1] || 'File';
const fileType = solution.type_solution;
if (fileType !== 'text' && solution.path_solution) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<Text>
{fileType === 'image' ? '[Image]' : '[Document]'} {fileName}
</Text>
<Button
type="link"
size="small"
onClick={() => onFileView(solution.path_solution, solution.type_solution)}
style={{ padding: 0, height: 'auto', fontSize: '12px' }}
>
View Document
</Button>
</div>
);
}
return null;
})()}
<Form.Item name={`text_solution_${fieldId}`} label="Text Solution">
<Input.TextArea
placeholder="Enter text solution"
disabled={isReadOnly}
rows={4}
/>
</Form.Item>
)}
) : (
<>
{/* Show existing file info for both preview and edit mode */}
{currentSolutionData &&
currentSolutionData.type_solution !== 'text' &&
currentSolutionData.path_solution && (
<Form.Item label="Current Document">
{(() => {
const solution = currentSolutionData;
const fileName =
solution.file_upload_name ||
solution.path_solution?.split('/')[1] ||
'File';
const fileType = solution.type_solution;
<Form.Item label="Upload File">
<Upload
multiple={true}
accept=".pdf,.jpg,.jpeg,.png,.gif"
disabled={isReadOnly}
fileList={[
...fileList.filter(file => file.solutionId === fieldId),
// Add existing file to fileList if it exists
...(currentSolutionData && currentSolutionData.type_solution !== 'text' && currentSolutionData.path_solution ? [{
uid: `existing-${fieldId}`,
name: currentSolutionData.file_upload_name || currentSolutionData.path_solution?.split('/')[1] || 'File',
status: 'done',
url: null, // We'll use the path_solution for viewing
solutionId: fieldId,
type_solution: currentSolutionData.type_solution,
uploadPath: currentSolutionData.path_solution,
existingFile: true
}] : [])
]}
onRemove={(file) => {
}}
beforeUpload={handleBeforeUpload}
>
<Button icon={<UploadOutlined />} disabled={isReadOnly}>
Click to Upload (File or Image)
</Button>
</Upload>
</Form.Item>
</>
);
if (fileType !== 'text' && solution.path_solution) {
return (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '8px',
}}
>
<Text>
{fileType === 'image'
? '[Image]'
: '[Document]'}{' '}
{fileName}
</Text>
<Button
type="link"
size="small"
onClick={() =>
onFileView(
solution.path_solution,
solution.type_solution
)
}
style={{
padding: 0,
height: 'auto',
fontSize: '12px',
}}
>
View Document
</Button>
</div>
);
}
return null;
})()}
</Form.Item>
)}
<Form.Item label="Upload File">
<Upload
multiple={true}
accept=".pdf,.jpg,.jpeg,.png,.gif"
disabled={isReadOnly}
fileList={[
...fileList.filter((file) => file.solutionId === fieldId),
// Add existing file to fileList if it exists
...(currentSolutionData &&
currentSolutionData.type_solution !== 'text' &&
currentSolutionData.path_solution
? [
{
uid: `existing-${fieldId}`,
name:
currentSolutionData.file_upload_name ||
currentSolutionData.path_solution?.split(
'/'
)[1] ||
'File',
status: 'done',
url: null, // We'll use the path_solution for viewing
solutionId: fieldId,
type_solution:
currentSolutionData.type_solution,
uploadPath: currentSolutionData.path_solution,
existingFile: true,
},
]
: []),
]}
onRemove={(file) => {}}
beforeUpload={handleBeforeUpload}
>
<Button icon={<UploadOutlined />} disabled={isReadOnly}>
Click to Upload (File or Image)
</Button>
</Upload>
</Form.Item>
</>
);
}}
</Form.Item>
</div>

View File

@@ -194,6 +194,9 @@ export const useErrorCodeLogic = (errorCodeForm, fileList) => {
};
const handleSolutionStatusChange = (fieldId, status) => {
// Update form immediately
errorCodeForm.setFieldValue(`solution_status_${fieldId}`, status);
// Then update local state
setSolutionStatuses(prev => ({
...prev,
[fieldId]: status

View File

@@ -181,7 +181,7 @@ const DetailUnit = (props) => {
<Input
name="unit_code"
value={formData.unit_code || ''}
placeholder="Dibuat otomatis oleh sistem"
placeholder="Unit Code Auto Fill"
disabled
style={{
backgroundColor: '#f5f5f5',