lavoce #11
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user