lavoce #11
@@ -5,9 +5,18 @@ const API_BASE_URL = import.meta.env.VITE_API_SERVER;
|
|||||||
|
|
||||||
// Get file from uploads directory
|
// Get file from uploads directory
|
||||||
const getFile = async (folder, filename) => {
|
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)}`, {
|
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;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { SendRequest } from '../components/Global/ApiRequest';
|
|||||||
const getAllJadwalShift = async (queryParams) => {
|
const getAllJadwalShift = async (queryParams) => {
|
||||||
const response = await SendRequest({
|
const response = await SendRequest({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
prefix: `jadwal-shift?${queryParams.toString()}`,
|
prefix: `user-schedule?${queryParams.toString()}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -12,7 +12,7 @@ const getAllJadwalShift = async (queryParams) => {
|
|||||||
const getJadwalShiftById = async (id) => {
|
const getJadwalShiftById = async (id) => {
|
||||||
const response = await SendRequest({
|
const response = await SendRequest({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
prefix: `jadwal-shift/${id}`,
|
prefix: `user-schedule/${id}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -21,7 +21,7 @@ const getJadwalShiftById = async (id) => {
|
|||||||
const createJadwalShift = async (queryParams) => {
|
const createJadwalShift = async (queryParams) => {
|
||||||
const response = await SendRequest({
|
const response = await SendRequest({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
prefix: `jadwal-shift`,
|
prefix: `user-schedule`,
|
||||||
params: queryParams,
|
params: queryParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ const createJadwalShift = async (queryParams) => {
|
|||||||
const updateJadwalShift = async (id, queryParams) => {
|
const updateJadwalShift = async (id, queryParams) => {
|
||||||
const response = await SendRequest({
|
const response = await SendRequest({
|
||||||
method: 'put',
|
method: 'put',
|
||||||
prefix: `jadwal-shift/${id}`,
|
prefix: `user-schedule/${id}`,
|
||||||
params: queryParams,
|
params: queryParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ const updateJadwalShift = async (id, queryParams) => {
|
|||||||
const deleteJadwalShift = async (id) => {
|
const deleteJadwalShift = async (id) => {
|
||||||
const response = await SendRequest({
|
const response = await SendRequest({
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
prefix: `jadwal-shift/${id}`,
|
prefix: `user-schedule/${id}`,
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const LayoutSidebar = () => {
|
|||||||
// console.log(collapsed, type);
|
// console.log(collapsed, type);
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(180deg, #FF8C42 0%, #FF6B35 100%)',
|
background: 'linear-gradient(180deg, #1BAA56 0%,rgb(5, 75, 34) 100%)',
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
|
|||||||
@@ -22,18 +22,18 @@ const ListHistoryEvent = memo(function ListHistoryEvent(props) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Tag Name',
|
title: 'Tag Name',
|
||||||
dataIndex: 'tag_name',
|
dataIndex: 'tagname',
|
||||||
key: 'tag_name',
|
key: 'tagname',
|
||||||
width: '40%',
|
width: '40%',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Description',
|
title: 'Description',
|
||||||
dataIndex: 'condition',
|
dataIndex: 'description',
|
||||||
key: 'condition',
|
key: 'description',
|
||||||
width: '20%',
|
width: '20%',
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Button type="text" style={{ backgroundColor: record.status_color, width: '100%' }}>
|
<Button type="text" style={{ backgroundColor: record.status_color, width: '100%' }}>
|
||||||
{record.condition}
|
{record.description}
|
||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,173 +1,278 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import { Modal, Select, Typography, Button, ConfigProvider } from 'antd';
|
||||||
Modal,
|
import { NotifOk } from '../../../components/Global/ToastNotif';
|
||||||
Typography,
|
import { createJadwalShift, updateJadwalShift } from '../../../api/jadwal-shift';
|
||||||
Button,
|
import { getAllUser } from '../../../api/user';
|
||||||
ConfigProvider,
|
import { getAllShift } from '../../../api/master-shift';
|
||||||
Form,
|
import { validateRun } from '../../../Utils/validate';
|
||||||
Select,
|
|
||||||
Spin,
|
|
||||||
Input
|
|
||||||
} from 'antd';
|
|
||||||
import { NotifOk, NotifAlert } from '../../../components/Global/ToastNotif';
|
|
||||||
import { updateJadwalShift, createJadwalShift } from '../../../api/jadwal-shift.jsx';
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
const DetailJadwalShift = (props) => {
|
const DetailJadwalShift = (props) => {
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
const [employees, setEmployees] = useState([]);
|
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 = () => {
|
const handleCancel = () => {
|
||||||
|
props.setSelectedData(null);
|
||||||
props.setActionMode('list');
|
props.setActionMode('list');
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchEmployees = async () => {
|
const fetchData = async () => {
|
||||||
setLoadingEmployees(true);
|
setLoadingData(true);
|
||||||
try {
|
try {
|
||||||
// Data dummy untuk dropdown karyawan
|
const params = new URLSearchParams({
|
||||||
const dummyEmployees = [
|
page: 1,
|
||||||
{ employee_id: '101', nama_employee: 'Andi Pratama' },
|
limit: 100,
|
||||||
{ employee_id: '102', nama_employee: 'Budi Santoso' },
|
});
|
||||||
{ employee_id: '103', nama_employee: 'Citra Lestari' },
|
|
||||||
{ employee_id: '104', nama_employee: 'Dewi Anggraini' },
|
const [usersResponse, shiftsResponse] = await Promise.all([
|
||||||
{ employee_id: '105', nama_employee: 'Eko Wahyudi' },
|
getAllUser(params),
|
||||||
{ employee_id: '106', nama_employee: 'Fitriani' },
|
getAllShift(params),
|
||||||
];
|
]);
|
||||||
setEmployees(dummyEmployees);
|
|
||||||
|
const userData = usersResponse?.data || usersResponse || [];
|
||||||
|
const shiftData = shiftsResponse?.data || shiftsResponse || [];
|
||||||
|
|
||||||
|
setEmployees(Array.isArray(userData) ? userData : []);
|
||||||
|
setShifts(Array.isArray(shiftData) ? shiftData : []);
|
||||||
} catch (error) {
|
} 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 {
|
} finally {
|
||||||
setLoadingEmployees(false);
|
setLoadingData(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
|
||||||
const values = await form.validateFields();
|
|
||||||
let payload;
|
|
||||||
let responseMessage;
|
|
||||||
|
|
||||||
setConfirmLoading(true);
|
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.';
|
|
||||||
}
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500)); // Simulasi API call
|
|
||||||
|
|
||||||
NotifOk({ icon: 'success', title: 'Berhasil', message: responseMessage });
|
// Daftar aturan validasi
|
||||||
props.setActionMode('list'); // Menutup modal dan memicu refresh di parent
|
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 payload = {
|
||||||
|
user_id: formData.user_id,
|
||||||
|
shift_id: formData.shift_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add schedule_id only if editing and it exists
|
||||||
|
if (props.actionMode === 'edit' && formData.schedule_id) {
|
||||||
|
payload.schedule_id = formData.schedule_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
const message = error.response?.data?.message || 'Gagal memperbarui jadwal.';
|
NotifOk({
|
||||||
NotifAlert({ icon: 'error', title: 'Gagal', message });
|
icon: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
message: error.message || 'Terjadi kesalahan pada server.',
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setConfirmLoading(false);
|
setConfirmLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Hanya jalankan jika modal untuk 'edit' atau 'preview' terbuka
|
|
||||||
if (props.showModal) {
|
if (props.showModal) {
|
||||||
fetchEmployees();
|
fetchData();
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.actionMode, props.showModal, props.selectedData, form]);
|
}, [props.showModal, props.selectedData, props.actionMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<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}
|
open={props.showModal}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
width={600}
|
|
||||||
footer={[
|
footer={[
|
||||||
<React.Fragment key="modal-footer">
|
<React.Fragment key="modal-footer">
|
||||||
<Button key="back" onClick={handleCancel}>
|
<ConfigProvider
|
||||||
{isReadOnly ? 'Tutup' : 'Batal'}
|
theme={{
|
||||||
</Button>
|
components: {
|
||||||
{!isReadOnly && (
|
Button: {
|
||||||
<Button key="submit" type="primary" loading={confirmLoading} onClick={handleSave} style={{ backgroundColor: '#23A55A' }}>
|
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
|
Simpan
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
</ConfigProvider>
|
||||||
</React.Fragment>,
|
</React.Fragment>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Spin spinning={loadingEmployees} tip="Memuat data...">
|
{formData && (
|
||||||
<Form form={form} layout="vertical" name="shift_form">
|
<div>
|
||||||
{props.actionMode === 'add' ? (
|
<div style={{ marginBottom: 12 }}>
|
||||||
<>
|
<Text strong>Nama Karyawan</Text>
|
||||||
<Form.Item
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
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
|
<Select
|
||||||
|
value={formData.user_id}
|
||||||
|
onChange={(value) => handleSelectChange('user_id', value)}
|
||||||
placeholder="Pilih karyawan"
|
placeholder="Pilih karyawan"
|
||||||
|
disabled={props.readOnly || loadingData}
|
||||||
|
loading={loadingData}
|
||||||
showSearch
|
showSearch
|
||||||
optionFilterProp="children"
|
optionFilterProp="children"
|
||||||
|
filterOption={(input, option) =>
|
||||||
|
option?.children?.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
style={{ width: '100%' }}
|
||||||
>
|
>
|
||||||
{employees.map(emp => (
|
{employees
|
||||||
<Option key={emp.employee_id} value={emp.employee_id}>{emp.nama_employee}</Option>
|
.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>
|
</Select>
|
||||||
</Form.Item>
|
</div>
|
||||||
</>
|
|
||||||
) : (
|
<div style={{ marginBottom: 12 }}>
|
||||||
<>
|
<Text strong>No. Telepon</Text>
|
||||||
<Form.Item
|
<div
|
||||||
name="employee_id"
|
style={{
|
||||||
label="Nama Karyawan"
|
padding: '8px 12px',
|
||||||
rules={[{ required: true, message: 'Nama karyawan wajib dipilih!' }]}
|
backgroundColor: '#f5f5f5',
|
||||||
|
borderRadius: '6px',
|
||||||
|
|
||||||
|
marginTop: '4px',
|
||||||
|
color: formData.user_phone ? '#000' : '#999',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Select placeholder="Pilih karyawan" disabled={isReadOnly} showSearch optionFilterProp="children">
|
{formData.user_phone || 'Pilih karyawan terlebih dahulu'}
|
||||||
{employees.map(emp => (
|
</div>
|
||||||
<Option key={emp.employee_id} value={emp.employee_id}>{emp.nama_employee}</Option>
|
</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>
|
</Select>
|
||||||
</Form.Item>
|
</div>
|
||||||
<Form.Item name="shift_name" label="Shift" rules={[{ required: true, message: 'Shift wajib dipilih!' }]}>
|
</div>
|
||||||
<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>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
import React, { memo, useState, useEffect } from 'react';
|
import React, { memo, useState, useEffect } from 'react';
|
||||||
import { Space, ConfigProvider, Button, Row, Col, Card, Input, Typography, Spin, Divider, Checkbox, Select } from 'antd';
|
import {
|
||||||
|
Space,
|
||||||
|
ConfigProvider,
|
||||||
|
Button,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Card,
|
||||||
|
Input,
|
||||||
|
Typography,
|
||||||
|
Divider,
|
||||||
|
Checkbox,
|
||||||
|
Select,
|
||||||
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
@@ -9,32 +21,25 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
|
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { getAllJadwalShift, deleteJadwalShift, updateJadwalShift } from '../../../api/jadwal-shift.jsx';
|
import { getAllJadwalShift, deleteJadwalShift, updateJadwalShift } from '../../../api/jadwal-shift';
|
||||||
|
import { getAllShift } from '../../../api/master-shift';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
const ListJadwalShift = memo(function ListJadwalShift(props) {
|
const ListJadwalShift = memo(function ListJadwalShift(props) {
|
||||||
|
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||||
|
const defaultFilter = { criteria: '' };
|
||||||
|
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||||
const [groupedSchedules, setGroupedSchedules] = useState({});
|
const [groupedSchedules, setGroupedSchedules] = useState({});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(false);
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
|
||||||
const [selectedSchedules, setSelectedSchedules] = useState([]);
|
const [selectedSchedules, setSelectedSchedules] = useState([]);
|
||||||
const [editingShift, setEditingShift] = useState(null); // State for shift-specific edit mode
|
const [editingShift, setEditingShift] = useState(null);
|
||||||
const [pendingChanges, setPendingChanges] = useState({}); // State for bulk shift edits
|
const [pendingChanges, setPendingChanges] = useState({});
|
||||||
const [employeeOptions, setEmployeeOptions] = useState([]); // State for employee dropdown
|
const [employeeOptions, setEmployeeOptions] = useState([]);
|
||||||
|
const [shiftOptions, setShiftOptions] = useState([]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Function to format timestamp without moment.js
|
|
||||||
const formatTimestamp = (timestamp) => {
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
const optionsDate = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
|
||||||
const optionsTime = { hour: '2-digit', minute: '2-digit', hour12: false };
|
|
||||||
|
|
||||||
const formattedDate = date.toLocaleDateString('id-ID', optionsDate);
|
|
||||||
const formattedTime = date.toLocaleTimeString('id-ID', optionsTime);
|
|
||||||
return `${formattedDate} pukul ${formattedTime}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatRelativeTimestamp = (timestamp) => {
|
const formatRelativeTimestamp = (timestamp) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
@@ -50,97 +55,157 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
|||||||
dayString = date.toLocaleDateString('id-ID', { day: 'numeric', month: 'long' });
|
dayString = date.toLocaleDateString('id-ID', { day: 'numeric', month: 'long' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeString = date.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', hour12: false }).replace('.', ':');
|
const timeString = date
|
||||||
|
.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', hour12: false })
|
||||||
|
.replace('.', ':');
|
||||||
return `${dayString}, ${timeString}`;
|
return `${dayString}, ${timeString}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// const params = new URLSearchParams({ ... });
|
const paging = {
|
||||||
// const response = await getAllJadwalShift(params);
|
page: 1,
|
||||||
|
limit: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
// ================== START: DUMMY DATA FOR VISUAL PREVIEW ==================
|
const params = new URLSearchParams({ ...paging, ...formDataFilter });
|
||||||
// This section creates dummy schedules and users to ensure all shifts are populated.
|
|
||||||
// The actual API call is commented out for now.
|
|
||||||
|
|
||||||
const mockSchedules = [
|
// Fetch both schedules and shifts data
|
||||||
{ schedule_id: 1, employee_id: '101', shift_name: 'PAGI', nama_employee: 'Andi Pratama', whatsapp: '081234567890', updated_by: 'Admin Super', updated_at: '2024-05-21T10:00:00' },
|
const [schedulesResponse, shiftsResponse] = await Promise.all([
|
||||||
{ schedule_id: 2, employee_id: '102', shift_name: 'PAGI', nama_employee: 'Budi Santoso', whatsapp: '081234567891', updated_by: 'Admin Super', updated_at: '2024-05-21T10:00:00' },
|
getAllJadwalShift(params),
|
||||||
{ schedule_id: 3, employee_id: '103', shift_name: 'SIANG', nama_employee: 'Citra Lestari', whatsapp: '081234567892', updated_by: 'John Doe', updated_at: '2024-05-21T09:45:00' },
|
getAllShift(params),
|
||||||
{ schedule_id: 4, employee_id: '104', shift_name: 'SIANG', nama_employee: 'Dewi Anggraini', whatsapp: '081234567893', updated_by: 'John Doe', updated_at: '2024-05-21T09:45:00' },
|
]);
|
||||||
{ schedule_id: 5, employee_id: '105', shift_name: 'MALAM', nama_employee: 'Eko Wahyudi', whatsapp: '081234567894', updated_by: 'Jane Smith', updated_at: '2024-05-20T22:15:00' },
|
|
||||||
{ schedule_id: 6, employee_id: '106', shift_name: 'MALAM', nama_employee: 'Fitriani', whatsapp: '081234567895', updated_by: 'Jane Smith', updated_at: '2024-05-20T22:15:00' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Dummy employee data for dropdowns
|
// Handle nested data structure from backend
|
||||||
const dummyEmployees = [
|
const rawData = schedulesResponse?.data || schedulesResponse || [];
|
||||||
{ employee_id: '101', nama_employee: 'Andi Pratama' },
|
const shifts = shiftsResponse?.data || shiftsResponse || [];
|
||||||
{ 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' },
|
|
||||||
];
|
|
||||||
setEmployeeOptions(dummyEmployees);
|
|
||||||
// =================== END: DUMMY DATA FOR VISUAL PREVIEW ===================
|
|
||||||
|
|
||||||
|
setShiftOptions(shifts);
|
||||||
|
|
||||||
const grouped = mockSchedules.reduce((acc, schedule) => {
|
// Parse backend response structure: [{ shift: { shift_id, shift_name, users: [...] } }]
|
||||||
const shiftName = schedule.shift_name.toUpperCase().trim();
|
const grouped = {};
|
||||||
if (!acc[shiftName]) {
|
const allUsers = [];
|
||||||
acc[shiftName] = { users: [], lastUpdate: { user: 'N/A', timestamp: '1970-01-01T00:00:00Z' } };
|
|
||||||
|
rawData.forEach((item) => {
|
||||||
|
if (item.shift && item.shift.shift_name) {
|
||||||
|
const shift = item.shift;
|
||||||
|
const shiftName = shift.shift_name.toUpperCase().trim();
|
||||||
|
|
||||||
|
// Initialize shift group
|
||||||
|
if (!grouped[shiftName]) {
|
||||||
|
grouped[shiftName] = {
|
||||||
|
shift_id: shift.shift_id,
|
||||||
|
users: [],
|
||||||
|
lastUpdate: { user: 'N/A', timestamp: '1970-01-01T00:00:00Z' },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
acc[shiftName].users.push(schedule);
|
|
||||||
|
|
||||||
// Find the latest update timestamp for the shift
|
// Process users in this shift
|
||||||
const currentUpdate = new Date(schedule.updated_at || schedule.created_at);
|
if (shift.users && Array.isArray(shift.users)) {
|
||||||
const lastUpdate = new Date(acc[shiftName].lastUpdate.timestamp);
|
shift.users.forEach((user) => {
|
||||||
|
const normalizedUser = {
|
||||||
|
id: user.user_schedule_id,
|
||||||
|
user_schedule_id: user.user_schedule_id,
|
||||||
|
user_id: user.user_id,
|
||||||
|
shift_id: shift.shift_id,
|
||||||
|
shift_name: shift.shift_name,
|
||||||
|
nama_employee: user.user_fullname || user.user_name || 'Unknown',
|
||||||
|
whatsapp: user.user_phone || '-',
|
||||||
|
user_fullname: user.user_fullname,
|
||||||
|
user_name: user.user_name,
|
||||||
|
user_phone: user.user_phone,
|
||||||
|
updated_at: user.updated_at,
|
||||||
|
created_at: user.created_at,
|
||||||
|
updated_by: user.updated_by,
|
||||||
|
};
|
||||||
|
|
||||||
|
grouped[shiftName].users.push(normalizedUser);
|
||||||
|
allUsers.push(normalizedUser);
|
||||||
|
|
||||||
|
// Update last update timestamp
|
||||||
|
const currentUpdate = new Date(
|
||||||
|
user.updated_at || user.created_at || new Date()
|
||||||
|
);
|
||||||
|
const lastUpdate = new Date(grouped[shiftName].lastUpdate.timestamp);
|
||||||
if (currentUpdate > lastUpdate) {
|
if (currentUpdate > lastUpdate) {
|
||||||
acc[shiftName].lastUpdate = {
|
grouped[shiftName].lastUpdate = {
|
||||||
user: schedule.updated_by || 'N/A',
|
user: user.updated_by || 'N/A',
|
||||||
timestamp: currentUpdate.toISOString()
|
timestamp: currentUpdate.toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return acc;
|
});
|
||||||
}, {});
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const finalGrouped = {
|
setEmployeeOptions(allUsers);
|
||||||
'PAGI': grouped['PAGI'] || { users: [], lastUpdate: { user: 'N/A', timestamp: new Date().toISOString() } },
|
|
||||||
'SIANG': grouped['SIANG'] || { users: [], lastUpdate: { user: 'N/A', timestamp: new Date().toISOString() } },
|
// Add empty shifts that don't have users yet
|
||||||
'MALAM': grouped['MALAM'] || { users: [], lastUpdate: { user: 'N/A', timestamp: new Date().toISOString() } },
|
shifts.forEach((shift) => {
|
||||||
|
const shiftName = shift.shift_name.toUpperCase().trim();
|
||||||
|
if (!grouped[shiftName]) {
|
||||||
|
grouped[shiftName] = {
|
||||||
|
shift_id: shift.shift_id,
|
||||||
|
users: [],
|
||||||
|
lastUpdate: { user: 'N/A', timestamp: new Date().toISOString() },
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
setGroupedSchedules(finalGrouped);
|
setGroupedSchedules(grouped);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing dummy data:', error);
|
NotifAlert({
|
||||||
NotifAlert({ // Changed to NotifAlert for consistency
|
icon: 'error',
|
||||||
icon: 'error', // Changed to error icon
|
title: 'Gagal Memuat Data',
|
||||||
title: 'Gagal Memuat Data', // Changed title
|
message: 'Terjadi kesalahan saat memuat data jadwal shift.',
|
||||||
message: 'Terjadi kesalahan saat memuat data jadwal shift.', // Changed message
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
// Add a small delay to simulate network loading
|
setLoading(false);
|
||||||
setTimeout(() => setLoading(false), 500);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
if (token) {
|
if (token) {
|
||||||
fetchData();
|
if (props.actionMode === 'list') {
|
||||||
|
setFormDataFilter(defaultFilter);
|
||||||
|
doFilter();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
navigate('/signin');
|
navigate('/signin');
|
||||||
}
|
}
|
||||||
}, [searchValue, props.actionMode]); // Refetch when searchValue changes or after modal closes
|
}, [props.actionMode]);
|
||||||
|
|
||||||
const handleSearch = (value) => {
|
useEffect(() => {
|
||||||
setSearchValue(value);
|
if (props.actionMode === 'list') {
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
}, [trigerFilter]);
|
||||||
|
|
||||||
|
const doFilter = () => {
|
||||||
|
setTrigerFilter((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
setFormDataFilter({ criteria: searchValue });
|
||||||
|
setTrigerFilter((prev) => !prev);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchClear = () => {
|
const handleSearchClear = () => {
|
||||||
setSearchValue('');
|
setSearchValue('');
|
||||||
|
setFormDataFilter({ criteria: '' });
|
||||||
|
setTrigerFilter((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showPreviewModal = (param) => {
|
||||||
|
props.setSelectedData(param);
|
||||||
|
props.setActionMode('preview');
|
||||||
|
};
|
||||||
|
|
||||||
|
const showEditModal = (param = null) => {
|
||||||
|
props.setSelectedData(param);
|
||||||
|
props.setActionMode('edit');
|
||||||
};
|
};
|
||||||
|
|
||||||
const showAddModal = (param = null) => {
|
const showAddModal = (param = null) => {
|
||||||
@@ -148,28 +213,31 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
|||||||
props.setActionMode('add');
|
props.setActionMode('add');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAction = (mode, record) => {
|
const showDeleteDialog = (param) => {
|
||||||
props.setSelectedData(record);
|
|
||||||
props.setActionMode(mode);
|
|
||||||
};
|
|
||||||
|
|
||||||
const showDeleteDialog = (user) => {
|
|
||||||
NotifConfirmDialog({
|
NotifConfirmDialog({
|
||||||
icon: 'question',
|
icon: 'question',
|
||||||
title: 'Konfirmasi Hapus',
|
title: 'Konfirmasi Hapus',
|
||||||
message: `Hapus jadwal untuk karyawan "${user.nama_employee}"?`,
|
message: `Jadwal untuk karyawan "${param.nama_employee}" akan dihapus?`,
|
||||||
onConfirm: () => handleDelete(user.schedule_id),
|
onConfirm: () => handleDelete(param.id),
|
||||||
|
onCancel: () => props.setSelectedData(null),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (schedule_id) => {
|
const handleDelete = async (id) => {
|
||||||
try {
|
const response = await deleteJadwalShift(id);
|
||||||
await deleteJadwalShift(schedule_id);
|
if (response.statusCode === 200) {
|
||||||
NotifOk({ icon: 'success', title: 'Berhasil', message: 'Jadwal berhasil dihapus.' });
|
NotifAlert({
|
||||||
fetchData(); // Refresh data
|
icon: 'success',
|
||||||
} catch (error) {
|
title: 'Berhasil',
|
||||||
console.error("Failed to delete schedule:", error);
|
message: 'Jadwal berhasil dihapus.',
|
||||||
NotifAlert({ icon: "error", title: "Gagal", message: "Gagal menghapus jadwal." });
|
});
|
||||||
|
doFilter();
|
||||||
|
} else {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: response?.message || 'Gagal menghapus jadwal.',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -185,52 +253,79 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
|||||||
setSelectedSchedules([]);
|
setSelectedSchedules([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectSchedule = (scheduleId, isChecked) => {
|
const handleSelectSchedule = (id, isChecked) => {
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
setSelectedSchedules(prev => [...prev, scheduleId]);
|
setSelectedSchedules((prev) => [...prev, id]);
|
||||||
} else {
|
} else {
|
||||||
setSelectedSchedules(prev => prev.filter(id => id !== scheduleId));
|
setSelectedSchedules((prev) => prev.filter((scheduleId) => scheduleId !== id));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkUpdateChange = (scheduleId, field, value) => {
|
const handleBulkUpdateChange = (scheduleId, field, value) => {
|
||||||
setPendingChanges(prev => ({
|
setPendingChanges((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[scheduleId]: {
|
[scheduleId]: {
|
||||||
...prev[scheduleId],
|
...prev[scheduleId],
|
||||||
[field]: value,
|
[field]: value,
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkSave = async () => {
|
const handleBulkSave = async () => {
|
||||||
if (Object.keys(pendingChanges).length === 0) {
|
if (Object.keys(pendingChanges).length === 0) {
|
||||||
NotifAlert({ icon: 'info', title: 'Tidak Ada Perubahan', message: 'Tidak ada perubahan untuk disimpan.' });
|
NotifAlert({
|
||||||
|
icon: 'info',
|
||||||
|
title: 'Tidak Ada Perubahan',
|
||||||
|
message: 'Tidak ada perubahan untuk disimpan.',
|
||||||
|
});
|
||||||
cancelShiftEditMode();
|
cancelShiftEditMode();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePromises = Object.keys(pendingChanges).map(scheduleId => {
|
const updatePromises = Object.keys(pendingChanges).map((id) => {
|
||||||
const originalSchedule = groupedSchedules[editingShift].users.find(u => u.schedule_id.toString() === scheduleId);
|
const originalSchedule = groupedSchedules[editingShift].users.find(
|
||||||
const updatedData = { ...originalSchedule, ...pendingChanges[scheduleId] };
|
(u) => u.id.toString() === id
|
||||||
// return updateJadwalShift(scheduleId, updatedData); // UNCOMMENT FOR REAL API
|
);
|
||||||
console.log(`Simulating update for schedule ${scheduleId}:`, updatedData); // DUMMY LOG
|
const changes = pendingChanges[id];
|
||||||
return Promise.resolve(); // DUMMY PROMISE
|
|
||||||
|
// Build payload according to backend schema
|
||||||
|
const payload = {
|
||||||
|
user_id: changes.user_id || originalSchedule.user_id,
|
||||||
|
shift_id: changes.shift_id || originalSchedule.shift_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (originalSchedule.schedule_id) {
|
||||||
|
payload.schedule_id = originalSchedule.schedule_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateJadwalShift(id, payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(updatePromises);
|
await Promise.all(updatePromises);
|
||||||
NotifOk({ icon: 'success', title: 'Berhasil', message: 'Semua perubahan berhasil disimpan.' });
|
NotifOk({
|
||||||
fetchData();
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
message: 'Semua perubahan berhasil disimpan.',
|
||||||
|
});
|
||||||
|
doFilter();
|
||||||
cancelShiftEditMode();
|
cancelShiftEditMode();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
NotifAlert({ icon: 'error', title: 'Gagal', message: 'Gagal menyimpan beberapa perubahan.' });
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: 'Gagal menyimpan beberapa perubahan.',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkDelete = () => {
|
const handleBulkDelete = () => {
|
||||||
if (selectedSchedules.length === 0) {
|
if (selectedSchedules.length === 0) {
|
||||||
NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Pilih setidaknya satu jadwal untuk dihapus.' });
|
NotifAlert({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Perhatian',
|
||||||
|
message: 'Pilih setidaknya satu jadwal untuk dihapus.',
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NotifConfirmDialog({
|
NotifConfirmDialog({
|
||||||
@@ -238,11 +333,13 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
|||||||
title: `Konfirmasi Hapus`,
|
title: `Konfirmasi Hapus`,
|
||||||
message: `Anda yakin ingin menghapus ${selectedSchedules.length} jadwal yang dipilih?`,
|
message: `Anda yakin ingin menghapus ${selectedSchedules.length} jadwal yang dipilih?`,
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await Promise.all(selectedSchedules.map(id => deleteJadwalShift(id)));
|
await Promise.all(selectedSchedules.map((id) => deleteJadwalShift(id)));
|
||||||
NotifOk({ icon: 'success', title: 'Berhasil', message: `${selectedSchedules.length} jadwal berhasil dihapus.` });
|
NotifOk({
|
||||||
fetchData();
|
icon: 'success',
|
||||||
// Exit both edit modes
|
title: 'Berhasil',
|
||||||
setIsEditMode(false);
|
message: `${selectedSchedules.length} jadwal berhasil dihapus.`,
|
||||||
|
});
|
||||||
|
doFilter();
|
||||||
setEditingShift(null);
|
setEditingShift(null);
|
||||||
setSelectedSchedules([]);
|
setSelectedSchedules([]);
|
||||||
},
|
},
|
||||||
@@ -257,56 +354,7 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
|||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={24}>
|
<Col xs={24}>
|
||||||
<Row justify="space-between" align="middle" gutter={[8, 8]}>
|
<Row justify="end" align="middle" gutter={[8, 8]}>
|
||||||
{isEditMode ? (
|
|
||||||
<Col span={24}>
|
|
||||||
<Row justify="space-between" align="middle">
|
|
||||||
<Text strong>Mode Edit Halaman</Text>
|
|
||||||
<Space wrap align="center">
|
|
||||||
<Button onClick={() => { setIsEditMode(false); setPendingChanges({}); setSelectedSchedules([]); }}>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
danger
|
|
||||||
onClick={handleBulkDelete}
|
|
||||||
disabled={selectedSchedules.length === 0}
|
|
||||||
>
|
|
||||||
Hapus yang Dipilih ({selectedSchedules.length})
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={handleBulkSave}
|
|
||||||
style={{ backgroundColor: '#23A55A', borderColor: '#23A55A' }}
|
|
||||||
>
|
|
||||||
Simpan Semua Perubahan
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Col>
|
|
||||||
<ConfigProvider
|
|
||||||
theme={{
|
|
||||||
components: {
|
|
||||||
Button: {
|
|
||||||
defaultBg: '#23a55a',
|
|
||||||
defaultColor: '#FFFFFF',
|
|
||||||
defaultBorderColor: '#23a55a',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
size="large"
|
|
||||||
onClick={() => { setIsEditMode(true); setEditingShift(null); setPendingChanges({}); setSelectedSchedules([]); }}
|
|
||||||
>
|
|
||||||
Edit Halaman
|
|
||||||
</Button>
|
|
||||||
</ConfigProvider>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} sm={24} md={12} lg={12}>
|
<Col xs={24} sm={24} md={12} lg={12}>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder="Cari berdasarkan nama..."
|
placeholder="Cari berdasarkan nama..."
|
||||||
@@ -333,30 +381,48 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
|||||||
size="large"
|
size="large"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Spin spinning={loading} tip="Memuat data...">
|
|
||||||
<div style={{ marginTop: '24px' }}>
|
<div style={{ marginTop: '24px' }}>
|
||||||
{(Object.keys(groupedSchedules).length === 0 && !loading) ? (
|
{loading ? (
|
||||||
|
<Text>Memuat data...</Text>
|
||||||
|
) : Object.keys(groupedSchedules).length === 0 ? (
|
||||||
<Text>Tidak ada data jadwal untuk ditampilkan.</Text>
|
<Text>Tidak ada data jadwal untuk ditampilkan.</Text>
|
||||||
) : (
|
) : (
|
||||||
Object.keys(groupedSchedules).map(shiftName => ( // Iterate through each shift (PAGI, SIANG, MALAM)
|
['SHIFT PAGI', 'SHIFT SORE', 'SHIFT MALAM']
|
||||||
<div key={shiftName} style={{ marginBottom: '32px' }}> {/* Container for each shift section */}
|
.filter((shiftName) => groupedSchedules[shiftName])
|
||||||
<Row justify="space-between" align="middle" style={{ paddingBottom: '12px', borderBottom: '1px solid #f0f0f0', marginBottom: '16px' }}>
|
.map((shiftName) => (
|
||||||
|
<div key={shiftName} style={{ marginBottom: '32px' }}>
|
||||||
|
{' '}
|
||||||
|
{/* Container for each shift section */}
|
||||||
|
<Row
|
||||||
|
justify="space-between"
|
||||||
|
align="middle"
|
||||||
|
style={{
|
||||||
|
paddingBottom: '12px',
|
||||||
|
borderBottom: '1px solid #f0f0f0',
|
||||||
|
marginBottom: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Col>
|
<Col>
|
||||||
<Title level={5} style={{ margin: 0 }}>
|
<Title level={5} style={{ margin: 0 }}>
|
||||||
SHIFT {shiftName} ({groupedSchedules[shiftName].users.length} Karyawan)
|
{shiftName} (
|
||||||
|
{groupedSchedules[shiftName].users.length} Karyawan)
|
||||||
</Title>
|
</Title>
|
||||||
</Col>
|
</Col>
|
||||||
{editingShift === shiftName ? (
|
{editingShift === shiftName ? (
|
||||||
<Col>
|
<Col>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={cancelShiftEditMode}>Batal</Button>
|
|
||||||
<Button
|
<Button
|
||||||
|
key="cancel"
|
||||||
|
onClick={cancelShiftEditMode}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
key="delete"
|
||||||
type="primary"
|
type="primary"
|
||||||
danger
|
danger
|
||||||
onClick={handleBulkDelete}
|
onClick={handleBulkDelete}
|
||||||
@@ -365,9 +431,13 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
|||||||
Hapus Dipilih ({selectedSchedules.length})
|
Hapus Dipilih ({selectedSchedules.length})
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
key="save"
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={handleBulkSave}
|
onClick={handleBulkSave}
|
||||||
style={{ backgroundColor: '#23A55A', borderColor: '#23A55A' }}
|
style={{
|
||||||
|
backgroundColor: '#23A55A',
|
||||||
|
borderColor: '#23A55A',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Simpan Perubahan
|
Simpan Perubahan
|
||||||
</Button>
|
</Button>
|
||||||
@@ -377,23 +447,26 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
|||||||
<Col>
|
<Col>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button
|
<Button
|
||||||
|
key="add"
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={() => showAddModal({ shift_name: shiftName })}
|
onClick={() => showAddModal()}
|
||||||
style={{ backgroundColor: '#23A55A', borderColor: '#23A55A' }}
|
style={{
|
||||||
disabled={editingShift !== null || isEditMode}
|
backgroundColor: '#23A55A',
|
||||||
|
borderColor: '#23A55A',
|
||||||
|
}}
|
||||||
|
disabled={editingShift !== null}
|
||||||
>
|
>
|
||||||
Tambah User
|
Tambah Jadwal Shift
|
||||||
</Button>
|
</Button>
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
|
key="edit-config"
|
||||||
theme={{
|
theme={{
|
||||||
components: {
|
components: {
|
||||||
Button: {
|
Button: {
|
||||||
defaultBg: 'white',
|
defaultBg: 'white',
|
||||||
defaultColor: '#23A55A',
|
defaultColor: '#23A55A',
|
||||||
defaultBorderColor: '#23A55A',
|
defaultBorderColor: '#23A55A',
|
||||||
defaultHoverColor: '#23A55A',
|
|
||||||
defaultHoverBorderColor: '#23A55A',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -401,7 +474,7 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
|||||||
<Button
|
<Button
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
onClick={() => handleShiftEditMode(shiftName)}
|
onClick={() => handleShiftEditMode(shiftName)}
|
||||||
disabled={editingShift !== null || isEditMode}
|
disabled={editingShift !== null}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
@@ -410,81 +483,321 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
|||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{/* Horizontal scrollable container for employee cards */}
|
{/* Horizontal scrollable container for employee cards */}
|
||||||
<div style={{ display: 'flex', overflowX: 'auto', gap: '16px', paddingBottom: '10px' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
overflowX: 'auto',
|
||||||
|
gap: '16px',
|
||||||
|
paddingTop: '8px',
|
||||||
|
paddingBottom: '10px',
|
||||||
|
minWidth: `${4 * (320 + 16)}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{groupedSchedules[shiftName].users.length > 0 ? (
|
{groupedSchedules[shiftName].users.length > 0 ? (
|
||||||
groupedSchedules[shiftName].users.map(user => (
|
groupedSchedules[shiftName].users.map((user) => (
|
||||||
<Card
|
<Card
|
||||||
key={user.nik}
|
key={user.id}
|
||||||
hoverable
|
hoverable
|
||||||
style={{
|
style={{
|
||||||
width: 320, height: 240, flexShrink: 0, textAlign: 'left', border: '1px solid #42AAFF',
|
width: 320,
|
||||||
opacity: (editingShift !== null && editingShift !== shiftName) ? 0.5 : 1, // Dim inactive shifts
|
height: 240,
|
||||||
pointerEvents: (editingShift !== null && editingShift !== shiftName) ? 'none' : 'auto' // Disable interaction on inactive shifts
|
flexShrink: 0,
|
||||||
|
textAlign: 'left',
|
||||||
|
border: '1px solid #42AAFF',
|
||||||
|
opacity:
|
||||||
|
editingShift !== null &&
|
||||||
|
editingShift !== shiftName
|
||||||
|
? 0.5
|
||||||
|
: 1,
|
||||||
|
pointerEvents:
|
||||||
|
editingShift !== null &&
|
||||||
|
editingShift !== shiftName
|
||||||
|
? 'none'
|
||||||
|
: 'auto',
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
body: { padding: '16px', height: '100%' },
|
||||||
}}
|
}}
|
||||||
bodyStyle={{ padding: '16px', height: '100%' }}
|
|
||||||
>
|
>
|
||||||
{isEditMode && editingShift === null && ( // Checkbox for global delete mode only
|
{editingShift === shiftName ? (
|
||||||
<Checkbox
|
|
||||||
style={{ position: 'absolute', top: 16, right: 16, zIndex: 10 }}
|
|
||||||
checked={selectedSchedules.includes(user.schedule_id)}
|
|
||||||
onChange={(e) => handleSelectSchedule(user.schedule_id, e.target.checked)}
|
|
||||||
onClick={(e) => e.stopPropagation()} // Prevent card click
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{editingShift === shiftName || (isEditMode && editingShift === null) ? ( // Global or Shift-specific Edit Mode
|
|
||||||
// EDIT MODE VIEW
|
// EDIT MODE VIEW
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
<div
|
||||||
<Space direction="vertical" style={{ width: '100%', marginTop: '24px' }}>
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
height: '100%',
|
||||||
|
gap: '12px',
|
||||||
|
padding: '16px 4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
paddingTop: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedSchedules.includes(
|
||||||
|
user.id
|
||||||
|
)}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleSelectSchedule(
|
||||||
|
user.id,
|
||||||
|
e.target.checked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
transform: 'scale(1.4)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '14px',
|
||||||
|
paddingRight: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Text
|
||||||
|
strong
|
||||||
|
style={{
|
||||||
|
fontSize: '11px',
|
||||||
|
color: '#8c8c8c',
|
||||||
|
display: 'block',
|
||||||
|
marginBottom: '6px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
KARYAWAN
|
||||||
|
</Text>
|
||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
placeholder="Pilih Karyawan"
|
placeholder="Pilih Karyawan"
|
||||||
optionFilterProp="children"
|
optionFilterProp="children"
|
||||||
defaultValue={user.employee_id}
|
defaultValue={user.user_id}
|
||||||
onChange={(value) => handleBulkUpdateChange(user.schedule_id, 'employee_id', value)}
|
onChange={(value) =>
|
||||||
|
handleBulkUpdateChange(
|
||||||
|
user.id,
|
||||||
|
'user_id',
|
||||||
|
value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
size="large"
|
||||||
>
|
>
|
||||||
{employeeOptions.map(emp => (
|
{employeeOptions.map(
|
||||||
<Select.Option key={emp.employee_id} value={emp.employee_id}>{emp.nama_employee}</Select.Option>
|
(emp) => (
|
||||||
))}
|
<Select.Option
|
||||||
</Select>
|
key={
|
||||||
<Select
|
emp.user_id ||
|
||||||
style={{ width: '100%' }}
|
emp.id
|
||||||
defaultValue={user.shift_name}
|
}
|
||||||
onChange={(value) => handleBulkUpdateChange(user.schedule_id, 'shift_name', value)}
|
value={
|
||||||
|
emp.user_id ||
|
||||||
|
emp.id
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Select.Option value="PAGI">PAGI</Select.Option>
|
{emp.user_fullname ||
|
||||||
<Select.Option value="SIANG">SIANG</Select.Option>
|
emp.user_name ||
|
||||||
<Select.Option value="MALAM">MALAM</Select.Option>
|
emp.nama_employee}
|
||||||
|
</Select.Option>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
</Space>
|
</div>
|
||||||
<Checkbox
|
<div>
|
||||||
style={{ position: 'absolute', top: 16, right: 16, zIndex: 10 }}
|
<Text
|
||||||
checked={selectedSchedules.includes(user.schedule_id)}
|
strong
|
||||||
onChange={(e) => handleSelectSchedule(user.schedule_id, e.target.checked)}
|
style={{
|
||||||
|
fontSize: '11px',
|
||||||
|
color: '#8c8c8c',
|
||||||
|
display: 'block',
|
||||||
|
marginBottom: '6px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
NO. TELEPON
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
value={
|
||||||
|
pendingChanges[user.id]
|
||||||
|
?.user_id
|
||||||
|
? employeeOptions.find(
|
||||||
|
(emp) =>
|
||||||
|
emp.user_id ===
|
||||||
|
pendingChanges[
|
||||||
|
user
|
||||||
|
.id
|
||||||
|
]?.user_id
|
||||||
|
)?.user_phone ||
|
||||||
|
user.whatsapp
|
||||||
|
: user.whatsapp
|
||||||
|
}
|
||||||
|
readOnly
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
'#f5f5f5',
|
||||||
|
color: '#595959',
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
}}
|
||||||
|
size="large"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text
|
||||||
|
strong
|
||||||
|
style={{
|
||||||
|
fontSize: '11px',
|
||||||
|
color: '#8c8c8c',
|
||||||
|
display: 'block',
|
||||||
|
marginBottom: '6px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
SHIFT
|
||||||
|
</Text>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="Pilih Shift"
|
||||||
|
optionFilterProp="children"
|
||||||
|
defaultValue={user.shift_id}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleBulkUpdateChange(
|
||||||
|
user.id,
|
||||||
|
'shift_id',
|
||||||
|
value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{shiftOptions.map(
|
||||||
|
(shift) => (
|
||||||
|
<Select.Option
|
||||||
|
key={
|
||||||
|
shift.shift_id
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
shift.shift_id
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
shift.shift_name
|
||||||
|
}
|
||||||
|
</Select.Option>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// NORMAL VIEW
|
// NORMAL VIEW
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between', height: '100%' }}>
|
<div
|
||||||
<div>
|
style={{
|
||||||
<Text strong ellipsis style={{
|
display: 'flex',
|
||||||
fontSize: '22px', display: 'inline-block', backgroundColor: '#42AAFF',
|
flexDirection: 'column',
|
||||||
color: '#FFFFFF', padding: '4px 8px', borderRadius: '4px', marginBottom: '8px'
|
justifyContent: 'space-between',
|
||||||
}}>{user.nama_employee}</Text>
|
height: '100%',
|
||||||
<Text style={{ fontSize: '18px', display: 'block' }}>{user.whatsapp}</Text>
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#42AAFF',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
padding: '9px 12px',
|
||||||
|
borderRadius: '15px',
|
||||||
|
marginBottom: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
strong
|
||||||
|
style={{
|
||||||
|
fontSize: '18px',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
display: 'block',
|
||||||
|
marginBottom: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{user.nama_employee}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{user.whatsapp}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
|
<div
|
||||||
<Text style={{ fontSize: '12px', display: 'block', lineHeight: '1.4' }}>
|
style={{
|
||||||
<Text strong>Terakhir diperbarui</Text> <br />
|
display: 'flex',
|
||||||
{formatRelativeTimestamp(groupedSchedules[shiftName].lastUpdate.timestamp)} <br />
|
justifyContent: 'space-between',
|
||||||
oleh {groupedSchedules[shiftName].lastUpdate.user}
|
alignItems: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
display: 'block',
|
||||||
|
lineHeight: '1.4',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text strong>
|
||||||
|
Terakhir diperbarui
|
||||||
|
</Text>{' '}
|
||||||
|
<br />
|
||||||
|
{formatRelativeTimestamp(
|
||||||
|
user.updated_at ||
|
||||||
|
user.created_at ||
|
||||||
|
new Date()
|
||||||
|
)}{' '}
|
||||||
|
<br />
|
||||||
|
oleh {user.updated_by || 'N/A'}
|
||||||
</Text>
|
</Text>
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="text" size="small" icon={<EyeOutlined />} onClick={() => handleAction('preview', user)} style={{ color: '#1890ff', borderColor: '#1890ff' }} />
|
<Button
|
||||||
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => handleAction('edit', user)} style={{ color: '#faad14', borderColor: '#faad14' }} />
|
type="text"
|
||||||
<Button danger type="text" size="small" icon={<DeleteOutlined />} onClick={() => showDeleteDialog(user)} style={{ borderColor: '#ff4d4f' }} />
|
size="small"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={() =>
|
||||||
|
showPreviewModal(user)
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
color: '#1890ff',
|
||||||
|
borderColor: '#1890ff',
|
||||||
|
}}
|
||||||
|
title="View"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() =>
|
||||||
|
showEditModal(user)
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
color: '#faad14',
|
||||||
|
borderColor: '#faad14',
|
||||||
|
}}
|
||||||
|
title="Edit"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() =>
|
||||||
|
showDeleteDialog(user)
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
borderColor: '#ff4d4f',
|
||||||
|
}}
|
||||||
|
title="Delete"
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -492,14 +805,15 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
|||||||
</Card>
|
</Card>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<Text type="secondary" style={{ marginLeft: '16px' }}>Tidak ada karyawan yang dijadwalkan untuk shift ini.</Text>
|
<Text type="secondary" style={{ marginLeft: '16px' }}>
|
||||||
|
Tidak ada karyawan yang dijadwalkan untuk shift ini.
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Spin>
|
|
||||||
</Card>
|
</Card>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const AddBrandDevice = () => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [formData, setFormData] = useState(defaultData);
|
const [formData, setFormData] = useState(defaultData);
|
||||||
const [errorCodes, setErrorCodes] = useState([]);
|
const [errorCodes, setErrorCodes] = useState([]);
|
||||||
|
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
solutionFields,
|
solutionFields,
|
||||||
@@ -49,16 +50,29 @@ const AddBrandDevice = () => {
|
|||||||
handleSolutionStatusChange,
|
handleSolutionStatusChange,
|
||||||
resetSolutionFields,
|
resetSolutionFields,
|
||||||
checkFirstSolutionValid,
|
checkFirstSolutionValid,
|
||||||
setSolutionsForExistingRecord
|
setSolutionsForExistingRecord,
|
||||||
} = useErrorCodeLogic(errorCodeForm, fileList);
|
} = useErrorCodeLogic(errorCodeForm, fileList);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBreadcrumbItems([
|
setBreadcrumbItems([
|
||||||
{ title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}>• Master</span> },
|
{ 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]);
|
}, [setBreadcrumbItems, navigate]);
|
||||||
|
|
||||||
@@ -71,25 +85,31 @@ const AddBrandDevice = () => {
|
|||||||
await brandForm.validateFields();
|
await brandForm.validateFields();
|
||||||
setCurrentStep(1);
|
setCurrentStep(1);
|
||||||
} catch (error) {
|
} 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 () => {
|
const handleFinish = async () => {
|
||||||
setConfirmLoading(true);
|
setConfirmLoading(true);
|
||||||
try {
|
try {
|
||||||
const transformedErrorCodes = errorCodes.map(ec => ({
|
const transformedErrorCodes = errorCodes.map((ec) => ({
|
||||||
error_code: ec.error_code,
|
error_code: ec.error_code,
|
||||||
error_code_name: ec.error_code_name || '',
|
error_code_name: ec.error_code_name || '',
|
||||||
error_code_description: ec.error_code_description || '',
|
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,
|
is_active: ec.status !== undefined ? ec.status : true,
|
||||||
solution: (ec.solution || []).map(sol => ({
|
solution: (ec.solution || []).map((sol) => ({
|
||||||
solution_name: sol.solution_name,
|
solution_name: sol.solution_name,
|
||||||
type_solution: sol.type_solution,
|
type_solution: sol.type_solution,
|
||||||
text_solution: sol.text_solution || '',
|
text_solution: sol.text_solution || '',
|
||||||
path_solution: sol.path_solution || '',
|
path_solution: sol.path_solution || '',
|
||||||
is_active: sol.is_active !== false
|
is_active: sol.is_active !== false,
|
||||||
}))
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const finalFormData = {
|
const finalFormData = {
|
||||||
@@ -98,23 +118,28 @@ const AddBrandDevice = () => {
|
|||||||
brand_model: formData.brand_model || '',
|
brand_model: formData.brand_model || '',
|
||||||
brand_manufacture: formData.brand_manufacture,
|
brand_manufacture: formData.brand_manufacture,
|
||||||
is_active: formData.is_active,
|
is_active: formData.is_active,
|
||||||
error_code: transformedErrorCodes.length > 0 ? transformedErrorCodes : [
|
error_code:
|
||||||
|
transformedErrorCodes.length > 0
|
||||||
|
? transformedErrorCodes
|
||||||
|
: [
|
||||||
{
|
{
|
||||||
error_code: "DEFAULT",
|
error_code: 'DEFAULT',
|
||||||
error_code_name: "Default Error Code",
|
error_code_name: 'Default Error Code',
|
||||||
error_code_description: "Default error description",
|
error_code_description: 'Default error description',
|
||||||
|
error_code_color: '#000000',
|
||||||
|
path_icon: '',
|
||||||
is_active: true,
|
is_active: true,
|
||||||
solution: [
|
solution: [
|
||||||
{
|
{
|
||||||
solution_name: "Default Solution",
|
solution_name: 'Default Solution',
|
||||||
type_solution: "text",
|
type_solution: 'text',
|
||||||
text_solution: "Default solution text",
|
text_solution: 'Default solution text',
|
||||||
path_solution: "",
|
path_solution: '',
|
||||||
is_active: true
|
is_active: true,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await createBrand(finalFormData);
|
const response = await createBrand(finalFormData);
|
||||||
@@ -135,9 +160,9 @@ const AddBrandDevice = () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: "error",
|
icon: 'error',
|
||||||
title: "Gagal",
|
title: 'Gagal',
|
||||||
message: error.message || "Gagal menyimpan data. Silakan coba lagi.",
|
message: error.message || 'Gagal menyimpan data. Silakan coba lagi.',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setConfirmLoading(false);
|
setConfirmLoading(false);
|
||||||
@@ -149,9 +174,11 @@ const AddBrandDevice = () => {
|
|||||||
error_code: record.error_code,
|
error_code: record.error_code,
|
||||||
error_code_name: record.error_code_name,
|
error_code_name: record.error_code_name,
|
||||||
error_code_description: record.error_code_description,
|
error_code_description: record.error_code_description,
|
||||||
|
error_code_color: record.error_code_color,
|
||||||
status: record.status,
|
status: record.status,
|
||||||
});
|
});
|
||||||
setFileList(record.fileList || []);
|
setFileList(record.fileList || []);
|
||||||
|
setErrorCodeIcon(record.errorCodeIcon || null);
|
||||||
setIsErrorCodeFormReadOnly(true);
|
setIsErrorCodeFormReadOnly(true);
|
||||||
setEditingErrorCodeKey(null);
|
setEditingErrorCodeKey(null);
|
||||||
|
|
||||||
@@ -165,9 +192,11 @@ const AddBrandDevice = () => {
|
|||||||
error_code: record.error_code,
|
error_code: record.error_code,
|
||||||
error_code_name: record.error_code_name,
|
error_code_name: record.error_code_name,
|
||||||
error_code_description: record.error_code_description,
|
error_code_description: record.error_code_description,
|
||||||
|
error_code_color: record.error_code_color,
|
||||||
status: record.status,
|
status: record.status,
|
||||||
});
|
});
|
||||||
setFileList(record.fileList || []);
|
setFileList(record.fileList || []);
|
||||||
|
setErrorCodeIcon(record.errorCodeIcon || null);
|
||||||
setIsErrorCodeFormReadOnly(false);
|
setIsErrorCodeFormReadOnly(false);
|
||||||
setEditingErrorCodeKey(record.key);
|
setEditingErrorCodeKey(record.key);
|
||||||
|
|
||||||
@@ -177,21 +206,29 @@ const AddBrandDevice = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddErrorCode = async (newErrorCode) => {
|
const handleAddErrorCode = async (newErrorCode) => {
|
||||||
|
// Include the current icon in the error code
|
||||||
|
const errorCodeWithIcon = {
|
||||||
|
...newErrorCode,
|
||||||
|
errorCodeIcon: errorCodeIcon
|
||||||
|
};
|
||||||
|
|
||||||
if (editingErrorCodeKey) {
|
if (editingErrorCodeKey) {
|
||||||
const updatedCodes = errorCodes.map(item => item.key === editingErrorCodeKey ? newErrorCode : item);
|
const updatedCodes = errorCodes.map((item) =>
|
||||||
|
item.key === editingErrorCodeKey ? errorCodeWithIcon : item
|
||||||
|
);
|
||||||
setErrorCodes(updatedCodes);
|
setErrorCodes(updatedCodes);
|
||||||
NotifOk({
|
NotifOk({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Berhasil',
|
title: 'Berhasil',
|
||||||
message: 'Error code berhasil diupdate!'
|
message: 'Error code berhasil diupdate!',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const updatedCodes = [...errorCodes, newErrorCode];
|
const updatedCodes = [...errorCodes, errorCodeWithIcon];
|
||||||
setErrorCodes(updatedCodes);
|
setErrorCodes(updatedCodes);
|
||||||
NotifOk({
|
NotifOk({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Berhasil',
|
title: 'Berhasil',
|
||||||
message: 'Error code berhasil ditambahkan!'
|
message: 'Error code berhasil ditambahkan!',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,9 +240,10 @@ const AddBrandDevice = () => {
|
|||||||
errorCodeForm.setFieldsValue({
|
errorCodeForm.setFieldsValue({
|
||||||
status: true,
|
status: true,
|
||||||
solution_status_0: true,
|
solution_status_0: true,
|
||||||
solution_type_0: 'text'
|
solution_type_0: 'text',
|
||||||
});
|
});
|
||||||
setFileList([]);
|
setFileList([]);
|
||||||
|
setErrorCodeIcon(null);
|
||||||
resetSolutionFields();
|
resetSolutionFields();
|
||||||
setIsErrorCodeFormReadOnly(false);
|
setIsErrorCodeFormReadOnly(false);
|
||||||
setEditingErrorCodeKey(null);
|
setEditingErrorCodeKey(null);
|
||||||
@@ -220,16 +258,16 @@ const AddBrandDevice = () => {
|
|||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
title: 'Perhatian',
|
title: 'Perhatian',
|
||||||
message: 'Setiap brand harus memiliki minimal 1 error code!'
|
message: 'Setiap brand harus memiliki minimal 1 error code!',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrorCodes(errorCodes.filter(item => item.key !== key));
|
setErrorCodes(errorCodes.filter((item) => item.key !== key));
|
||||||
NotifOk({
|
NotifOk({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Berhasil',
|
title: 'Berhasil',
|
||||||
message: 'Error code berhasil dihapus!'
|
message: 'Error code berhasil dihapus!',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -248,12 +286,17 @@ const AddBrandDevice = () => {
|
|||||||
|
|
||||||
const handleSolutionFileUpload = async (file) => {
|
const handleSolutionFileUpload = async (file) => {
|
||||||
try {
|
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) {
|
if (!isAllowedType) {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`
|
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -271,17 +314,17 @@ const AddBrandDevice = () => {
|
|||||||
file.solution_name = file.name;
|
file.solution_name = file.name;
|
||||||
file.solutionId = solutionFields[0];
|
file.solutionId = solutionFields[0];
|
||||||
file.type_solution = fileType;
|
file.type_solution = fileType;
|
||||||
setFileList(prevList => [...prevList, file]);
|
setFileList((prevList) => [...prevList, file]);
|
||||||
NotifOk({
|
NotifOk({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Berhasil',
|
title: 'Berhasil',
|
||||||
message: `${file.name} berhasil diupload!`
|
message: `${file.name} berhasil diupload!`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Gagal',
|
title: 'Gagal',
|
||||||
message: `Gagal mengupload ${file.name}`
|
message: `Gagal mengupload ${file.name}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -289,23 +332,33 @@ const AddBrandDevice = () => {
|
|||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
message: `Gagal mengupload ${file.name}. Silakan coba lagi.`
|
message: `Gagal mengupload ${file.name}. Silakan coba lagi.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileRemove = (file) => {
|
const handleFileRemove = (file) => {
|
||||||
const newFileList = fileList.filter(item => item.uid !== file.uid);
|
const newFileList = fileList.filter((item) => item.uid !== file.uid);
|
||||||
setFileList(newFileList);
|
setFileList(newFileList);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleErrorCodeIconUpload = (iconData) => {
|
||||||
|
setErrorCodeIcon(iconData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleErrorCodeIconRemove = () => {
|
||||||
|
setErrorCodeIcon(null);
|
||||||
|
};
|
||||||
|
|
||||||
const renderStepContent = () => {
|
const renderStepContent = () => {
|
||||||
if (currentStep === 0) {
|
if (currentStep === 0) {
|
||||||
return (
|
return (
|
||||||
<BrandForm
|
<BrandForm
|
||||||
form={brandForm}
|
form={brandForm}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onValuesChange={(changedValues, allValues) => setFormData(prev => ({...prev, ...allValues}))}
|
onValuesChange={(changedValues, allValues) =>
|
||||||
|
setFormData((prev) => ({ ...prev, ...allValues }))
|
||||||
|
}
|
||||||
isEdit={false}
|
isEdit={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -314,17 +367,22 @@ const AddBrandDevice = () => {
|
|||||||
if (currentStep === 1) {
|
if (currentStep === 1) {
|
||||||
return (
|
return (
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
<Col span={12}>
|
<Col span={8}>
|
||||||
<Title level={5} style={{ marginBottom: 16 }}>
|
<Title level={5} style={{ marginBottom: 16 }}>
|
||||||
{isErrorCodeFormReadOnly
|
{isErrorCodeFormReadOnly
|
||||||
? 'View Error Code'
|
? 'View Error Code'
|
||||||
: (editingErrorCodeKey ? 'Edit Error Code' : 'Tambah Error Code')
|
: editingErrorCodeKey
|
||||||
}
|
? 'Edit Error Code'
|
||||||
|
: 'Tambah Error Code'}
|
||||||
</Title>
|
</Title>
|
||||||
<Form
|
<Form
|
||||||
form={errorCodeForm}
|
form={errorCodeForm}
|
||||||
layout="vertical"
|
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}
|
onValuesChange={checkFirstSolutionValid}
|
||||||
>
|
>
|
||||||
<ErrorCodeForm
|
<ErrorCodeForm
|
||||||
@@ -347,10 +405,13 @@ const AddBrandDevice = () => {
|
|||||||
onCreateNewErrorCode={handleCreateNewErrorCode}
|
onCreateNewErrorCode={handleCreateNewErrorCode}
|
||||||
onResetForm={resetErrorCodeForm}
|
onResetForm={resetErrorCodeForm}
|
||||||
errorCodes={errorCodes}
|
errorCodes={errorCodes}
|
||||||
|
errorCodeIcon={errorCodeIcon}
|
||||||
|
onErrorCodeIconUpload={handleErrorCodeIconUpload}
|
||||||
|
onErrorCodeIconRemove={handleErrorCodeIconRemove}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={16}>
|
||||||
<ErrorCodeTable
|
<ErrorCodeTable
|
||||||
errorCodes={errorCodes}
|
errorCodes={errorCodes}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@@ -368,14 +429,14 @@ const AddBrandDevice = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<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 }}>
|
<Steps current={currentStep} style={{ marginBottom: 24 }}>
|
||||||
<Step title="Brand Device Details" />
|
<Step title="Brand Device Details" />
|
||||||
<Step title="Error Codes" />
|
<Step title="Error Codes" />
|
||||||
</Steps>
|
</Steps>
|
||||||
<div style={{ marginTop: 24 }}>
|
<div style={{ marginTop: 24 }}>{renderStepContent()}</div>
|
||||||
{renderStepContent()}
|
|
||||||
</div>
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<FormActions
|
<FormActions
|
||||||
currentStep={currentStep}
|
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 { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
|
||||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
||||||
import { getBrandById, updateBrand } from '../../../api/master-brand';
|
import { getBrandById, updateBrand } from '../../../api/master-brand';
|
||||||
|
import { getFileUrl } from '../../../api/file-uploads';
|
||||||
import BrandForm from './component/BrandForm';
|
import BrandForm from './component/BrandForm';
|
||||||
import ErrorCodeForm from './component/ErrorCodeForm';
|
import ErrorCodeForm from './component/ErrorCodeForm';
|
||||||
import ErrorCodeTable from './component/ListErrorCode';
|
import ErrorCodeTable from './component/ListErrorCode';
|
||||||
@@ -37,6 +38,7 @@ const EditBrandDevice = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [formData, setFormData] = useState(defaultData);
|
const [formData, setFormData] = useState(defaultData);
|
||||||
const [errorCodes, setErrorCodes] = useState([]);
|
const [errorCodes, setErrorCodes] = useState([]);
|
||||||
|
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
solutionFields,
|
solutionFields,
|
||||||
@@ -50,7 +52,7 @@ const EditBrandDevice = () => {
|
|||||||
handleSolutionStatusChange,
|
handleSolutionStatusChange,
|
||||||
resetSolutionFields,
|
resetSolutionFields,
|
||||||
checkFirstSolutionValid,
|
checkFirstSolutionValid,
|
||||||
setSolutionsForExistingRecord
|
setSolutionsForExistingRecord,
|
||||||
} = useErrorCodeLogic(errorCodeForm, fileList);
|
} = useErrorCodeLogic(errorCodeForm, fileList);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -61,7 +63,8 @@ const EditBrandDevice = () => {
|
|||||||
return;
|
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) {
|
if (savedPhase) {
|
||||||
setCurrentStep(parseInt(savedPhase));
|
setCurrentStep(parseInt(savedPhase));
|
||||||
localStorage.removeItem(`brand_device_edit_${id}_last_phase`);
|
localStorage.removeItem(`brand_device_edit_${id}_last_phase`);
|
||||||
@@ -70,9 +73,22 @@ const EditBrandDevice = () => {
|
|||||||
setBreadcrumbItems([
|
setBreadcrumbItems([
|
||||||
{ title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}>• Master</span> },
|
{ 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 {
|
try {
|
||||||
@@ -90,20 +106,34 @@ const EditBrandDevice = () => {
|
|||||||
brand_code: brandData.brand_code,
|
brand_code: brandData.brand_code,
|
||||||
};
|
};
|
||||||
|
|
||||||
const existingErrorCodes = brandData.error_code ? brandData.error_code.map((ec, index) => ({
|
const existingErrorCodes = brandData.error_code
|
||||||
|
? brandData.error_code.map((ec, index) => ({
|
||||||
key: `existing-${ec.error_code_id}`,
|
key: `existing-${ec.error_code_id}`,
|
||||||
error_code_id: ec.error_code_id,
|
error_code_id: ec.error_code_id,
|
||||||
error_code: ec.error_code,
|
error_code: ec.error_code,
|
||||||
error_code_name: ec.error_code_name || '',
|
error_code_name: ec.error_code_name || '',
|
||||||
error_code_description: ec.error_code_description || '',
|
error_code_description: ec.error_code_description || '',
|
||||||
|
error_code_color: ec.error_code_color || '#000000',
|
||||||
|
path_icon: ec.path_icon || '',
|
||||||
status: ec.is_active,
|
status: ec.is_active,
|
||||||
solution: ec.solution || []
|
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);
|
setFormData(newFormData);
|
||||||
brandForm.setFieldsValue(newFormData);
|
brandForm.setFieldsValue(newFormData);
|
||||||
setErrorCodes(existingErrorCodes);
|
setErrorCodes(existingErrorCodes);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
@@ -135,7 +165,11 @@ const EditBrandDevice = () => {
|
|||||||
await brandForm.validateFields();
|
await brandForm.validateFields();
|
||||||
setCurrentStep(1);
|
setCurrentStep(1);
|
||||||
} catch (error) {
|
} 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_model: formData.brand_model || '',
|
||||||
brand_manufacture: formData.brand_manufacture,
|
brand_manufacture: formData.brand_manufacture,
|
||||||
is_active: formData.is_active,
|
is_active: formData.is_active,
|
||||||
error_code: errorCodes.map(ec => ({
|
error_code: errorCodes.map((ec) => ({
|
||||||
error_code: ec.error_code,
|
error_code: ec.error_code,
|
||||||
error_code_name: ec.error_code_name || '',
|
error_code_name: ec.error_code_name || '',
|
||||||
error_code_description: ec.error_code_description || '',
|
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,
|
is_active: ec.status !== undefined ? ec.status : true,
|
||||||
solution: (ec.solution || []).map(sol => ({
|
solution: (ec.solution || []).map((sol) => ({
|
||||||
solution_name: sol.solution_name,
|
solution_name: sol.solution_name,
|
||||||
type_solution: sol.type_solution,
|
type_solution: sol.type_solution,
|
||||||
text_solution: sol.text_solution || '',
|
text_solution: sol.text_solution || '',
|
||||||
path_solution: sol.path_solution || '',
|
path_solution: sol.path_solution || '',
|
||||||
is_active: sol.is_active !== false
|
is_active: sol.is_active !== false,
|
||||||
}))
|
})),
|
||||||
}))
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await updateBrand(id, finalFormData);
|
const response = await updateBrand(id, finalFormData);
|
||||||
@@ -182,9 +218,9 @@ const EditBrandDevice = () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: "error",
|
icon: 'error',
|
||||||
title: "Gagal",
|
title: 'Gagal',
|
||||||
message: error.message || "Gagal mengupdate data. Silakan coba lagi.",
|
message: error.message || 'Gagal mengupdate data. Silakan coba lagi.',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setConfirmLoading(false);
|
setConfirmLoading(false);
|
||||||
@@ -196,8 +232,10 @@ const EditBrandDevice = () => {
|
|||||||
error_code: record.error_code,
|
error_code: record.error_code,
|
||||||
error_code_name: record.error_code_name,
|
error_code_name: record.error_code_name,
|
||||||
error_code_description: record.error_code_description,
|
error_code_description: record.error_code_description,
|
||||||
|
error_code_color: record.error_code_color,
|
||||||
status: record.status,
|
status: record.status,
|
||||||
});
|
});
|
||||||
|
setErrorCodeIcon(record.errorCodeIcon || null);
|
||||||
setIsErrorCodeFormReadOnly(true);
|
setIsErrorCodeFormReadOnly(true);
|
||||||
setEditingErrorCodeKey(record.key);
|
setEditingErrorCodeKey(record.key);
|
||||||
|
|
||||||
@@ -211,8 +249,10 @@ const EditBrandDevice = () => {
|
|||||||
error_code: record.error_code,
|
error_code: record.error_code,
|
||||||
error_code_name: record.error_code_name,
|
error_code_name: record.error_code_name,
|
||||||
error_code_description: record.error_code_description,
|
error_code_description: record.error_code_description,
|
||||||
|
error_code_color: record.error_code_color,
|
||||||
status: record.status,
|
status: record.status,
|
||||||
});
|
});
|
||||||
|
setErrorCodeIcon(record.errorCodeIcon || null);
|
||||||
setIsErrorCodeFormReadOnly(false);
|
setIsErrorCodeFormReadOnly(false);
|
||||||
setEditingErrorCodeKey(record.key);
|
setEditingErrorCodeKey(record.key);
|
||||||
|
|
||||||
@@ -226,22 +266,29 @@ const EditBrandDevice = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleAddErrorCode = (newErrorCode) => {
|
const handleAddErrorCode = (newErrorCode) => {
|
||||||
|
// Include the current icon in the error code
|
||||||
|
const errorCodeWithIcon = {
|
||||||
|
...newErrorCode,
|
||||||
|
errorCodeIcon: errorCodeIcon
|
||||||
|
};
|
||||||
|
|
||||||
let updatedErrorCodes;
|
let updatedErrorCodes;
|
||||||
if (editingErrorCodeKey) {
|
if (editingErrorCodeKey) {
|
||||||
updatedErrorCodes = errorCodes.map(item => item.key === editingErrorCodeKey ? newErrorCode : item);
|
updatedErrorCodes = errorCodes.map((item) =>
|
||||||
|
item.key === editingErrorCodeKey ? errorCodeWithIcon : item
|
||||||
|
);
|
||||||
NotifOk({
|
NotifOk({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Berhasil',
|
title: 'Berhasil',
|
||||||
message: 'Error code berhasil diupdate!'
|
message: 'Error code berhasil diupdate!',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
updatedErrorCodes = [...errorCodes, newErrorCode];
|
updatedErrorCodes = [...errorCodes, errorCodeWithIcon];
|
||||||
NotifOk({
|
NotifOk({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Berhasil',
|
title: 'Berhasil',
|
||||||
message: 'Error code berhasil ditambahkan!'
|
message: 'Error code berhasil ditambahkan!',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,9 +301,10 @@ const EditBrandDevice = () => {
|
|||||||
errorCodeForm.setFieldsValue({
|
errorCodeForm.setFieldsValue({
|
||||||
status: true,
|
status: true,
|
||||||
solution_status_0: true,
|
solution_status_0: true,
|
||||||
solution_type_0: 'text'
|
solution_type_0: 'text',
|
||||||
});
|
});
|
||||||
setFileList([]);
|
setFileList([]);
|
||||||
|
setErrorCodeIcon(null);
|
||||||
resetSolutionFields();
|
resetSolutionFields();
|
||||||
setIsErrorCodeFormReadOnly(false);
|
setIsErrorCodeFormReadOnly(false);
|
||||||
setEditingErrorCodeKey(null);
|
setEditingErrorCodeKey(null);
|
||||||
@@ -267,17 +315,17 @@ const EditBrandDevice = () => {
|
|||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
title: 'Perhatian',
|
title: 'Perhatian',
|
||||||
message: 'Setiap brand harus memiliki minimal 1 error code!'
|
message: 'Setiap brand harus memiliki minimal 1 error code!',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedErrorCodes = errorCodes.filter(item => item.key !== key);
|
const updatedErrorCodes = errorCodes.filter((item) => item.key !== key);
|
||||||
setErrorCodes(updatedErrorCodes);
|
setErrorCodes(updatedErrorCodes);
|
||||||
NotifOk({
|
NotifOk({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Berhasil',
|
title: 'Berhasil',
|
||||||
message: 'Error code berhasil dihapus!'
|
message: 'Error code berhasil dihapus!',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -285,6 +333,14 @@ const EditBrandDevice = () => {
|
|||||||
resetErrorCodeForm();
|
resetErrorCodeForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleErrorCodeIconUpload = (iconData) => {
|
||||||
|
setErrorCodeIcon(iconData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleErrorCodeIconRemove = () => {
|
||||||
|
setErrorCodeIcon(null);
|
||||||
|
};
|
||||||
|
|
||||||
const handleFileView = (pathSolution, fileType) => {
|
const handleFileView = (pathSolution, fileType) => {
|
||||||
localStorage.setItem(`brand_device_edit_${id}_last_phase`, currentStep.toString());
|
localStorage.setItem(`brand_device_edit_${id}_last_phase`, currentStep.toString());
|
||||||
|
|
||||||
@@ -297,7 +353,7 @@ const EditBrandDevice = () => {
|
|||||||
editingErrorCodeKey: editingErrorCodeKey,
|
editingErrorCodeKey: editingErrorCodeKey,
|
||||||
isErrorCodeFormReadOnly: isErrorCodeFormReadOnly,
|
isErrorCodeFormReadOnly: isErrorCodeFormReadOnly,
|
||||||
solutionsToDelete: Array.from(solutionsToDelete),
|
solutionsToDelete: Array.from(solutionsToDelete),
|
||||||
currentSolutionData: window.currentSolutionData || {}
|
currentSolutionData: window.currentSolutionData || {},
|
||||||
};
|
};
|
||||||
localStorage.setItem(`brand_device_edit_${id}_temp_data`, JSON.stringify(tempData));
|
localStorage.setItem(`brand_device_edit_${id}_temp_data`, JSON.stringify(tempData));
|
||||||
|
|
||||||
@@ -314,11 +370,11 @@ const EditBrandDevice = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSolutionFileUpload = (file) => {
|
const handleSolutionFileUpload = (file) => {
|
||||||
setFileList(prevList => [...prevList, file]);
|
setFileList((prevList) => [...prevList, file]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileRemove = (file) => {
|
const handleFileRemove = (file) => {
|
||||||
const newFileList = fileList.filter(item => item.uid !== file.uid);
|
const newFileList = fileList.filter((item) => item.uid !== file.uid);
|
||||||
setFileList(newFileList);
|
setFileList(newFileList);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -328,7 +384,9 @@ const EditBrandDevice = () => {
|
|||||||
<BrandForm
|
<BrandForm
|
||||||
form={brandForm}
|
form={brandForm}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onValuesChange={(changedValues, allValues) => setFormData(prev => ({...prev, ...allValues}))}
|
onValuesChange={(changedValues, allValues) =>
|
||||||
|
setFormData((prev) => ({ ...prev, ...allValues }))
|
||||||
|
}
|
||||||
isEdit={true}
|
isEdit={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -337,17 +395,24 @@ const EditBrandDevice = () => {
|
|||||||
if (currentStep === 1) {
|
if (currentStep === 1) {
|
||||||
return (
|
return (
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
<Col span={12}>
|
<Col span={8}>
|
||||||
<Title level={5} style={{ marginBottom: 16 }}>
|
<Title level={5} style={{ marginBottom: 16 }}>
|
||||||
{isErrorCodeFormReadOnly
|
{isErrorCodeFormReadOnly
|
||||||
? (editingErrorCodeKey ? 'View Error Code' : 'Error Code Form')
|
? editingErrorCodeKey
|
||||||
: (editingErrorCodeKey ? 'Edit Error Code' : 'Tambah Error Code')
|
? 'View Error Code'
|
||||||
}
|
: 'Error Code Form'
|
||||||
|
: editingErrorCodeKey
|
||||||
|
? 'Edit Error Code'
|
||||||
|
: 'Tambah Error Code'}
|
||||||
</Title>
|
</Title>
|
||||||
<Form
|
<Form
|
||||||
form={errorCodeForm}
|
form={errorCodeForm}
|
||||||
layout="vertical"
|
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}
|
onValuesChange={checkFirstSolutionValid}
|
||||||
>
|
>
|
||||||
<ErrorCodeForm
|
<ErrorCodeForm
|
||||||
@@ -370,19 +435,23 @@ const EditBrandDevice = () => {
|
|||||||
onCreateNewErrorCode={handleCreateNewErrorCode}
|
onCreateNewErrorCode={handleCreateNewErrorCode}
|
||||||
onResetForm={resetErrorCodeForm}
|
onResetForm={resetErrorCodeForm}
|
||||||
errorCodes={errorCodes}
|
errorCodes={errorCodes}
|
||||||
|
errorCodeIcon={errorCodeIcon}
|
||||||
|
onErrorCodeIconUpload={handleErrorCodeIconUpload}
|
||||||
|
onErrorCodeIconRemove={handleErrorCodeIconRemove}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={16}>
|
||||||
<ErrorCodeTable
|
<ErrorCodeTable
|
||||||
errorCodes={loading ?
|
errorCodes={
|
||||||
Array.from({ length: 3 }, (_, index) => ({
|
loading
|
||||||
|
? Array.from({ length: 3 }, (_, index) => ({
|
||||||
key: `loading-${index}`,
|
key: `loading-${index}`,
|
||||||
error_code: 'Loading...',
|
error_code: 'Loading...',
|
||||||
error_code_name: 'Loading...',
|
error_code_name: 'Loading...',
|
||||||
solution: []
|
solution: [],
|
||||||
})) :
|
}))
|
||||||
errorCodes
|
: errorCodes
|
||||||
}
|
}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onPreview={handlePreviewErrorCode}
|
onPreview={handlePreviewErrorCode}
|
||||||
@@ -399,14 +468,17 @@ const EditBrandDevice = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<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 }}>
|
<Steps current={currentStep} style={{ marginBottom: 24 }}>
|
||||||
<Step title="Brand Device Details" />
|
<Step title="Brand Device Details" />
|
||||||
<Step title="Error Codes" />
|
<Step title="Error Codes" />
|
||||||
</Steps>
|
</Steps>
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div style={{
|
<div
|
||||||
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -419,12 +491,18 @@ const EditBrandDevice = () => {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
borderRadius: '8px'
|
borderRadius: '8px',
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Spin size="large" />
|
<Spin size="large" />
|
||||||
</div>
|
</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()}
|
{renderStepContent()}
|
||||||
</div>
|
</div>
|
||||||
</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 { 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 { Typography, Card, Row, Col, Tag, Button, Space, Descriptions, Divider, Steps, Collapse, Switch, Spin, Modal, Empty } from 'antd';
|
||||||
import { ArrowLeftOutlined, EditOutlined, DeleteOutlined, FileTextOutlined, FilePdfOutlined, EyeOutlined } from '@ant-design/icons';
|
import { ArrowLeftOutlined, FileTextOutlined, FilePdfOutlined, EyeOutlined } from '@ant-design/icons';
|
||||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
||||||
import { NotifConfirmDialog, NotifOk, NotifAlert } from '../../../components/Global/ToastNotif';
|
import { NotifOk, NotifAlert } from '../../../components/Global/ToastNotif';
|
||||||
import { getBrandById, deleteBrand } from '../../../api/master-brand';
|
import { getBrandById } from '../../../api/master-brand';
|
||||||
import TableList from '../../../components/Global/TableList';
|
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
const { Step } = Steps;
|
const { Step } = Steps;
|
||||||
@@ -19,8 +18,7 @@ const ViewBrandDevice = () => {
|
|||||||
const [brandData, setBrandData] = useState(null);
|
const [brandData, setBrandData] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
const [errorCodesTriger, setErrorCodesTriger] = useState(0);
|
const [activeErrorKeys, setActiveErrorKeys] = useState([]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchBrandData = async () => {
|
const fetchBrandData = async () => {
|
||||||
@@ -44,10 +42,8 @@ const ViewBrandDevice = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await getBrandById(id);
|
const response = await getBrandById(id);
|
||||||
|
|
||||||
|
|
||||||
if (response && response.statusCode === 200) {
|
if (response && response.statusCode === 200) {
|
||||||
setBrandData(response.data);
|
setBrandData(response.data);
|
||||||
setErrorCodesTriger(prev => prev + 1);
|
|
||||||
} else {
|
} else {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
@@ -73,264 +69,21 @@ const ViewBrandDevice = () => {
|
|||||||
fetchBrandData();
|
fetchBrandData();
|
||||||
}, [id, setBreadcrumbItems, navigate, location.state]);
|
}, [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) => {
|
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());
|
localStorage.setItem(`brand_device_${id}_last_phase`, currentStep.toString());
|
||||||
|
|
||||||
// Extract only the filename without folder prefix
|
|
||||||
let actualFileName = fileName;
|
let actualFileName = fileName;
|
||||||
if (fileName && fileName.includes('/')) {
|
if (fileName && fileName.includes('/')) {
|
||||||
const parts = fileName.split('/');
|
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 encodedFileName = encodeURIComponent(actualFileName);
|
||||||
const fileTypeParam = fileType === 'image' ? 'image' : 'pdf';
|
const fileTypeParam = fileType === 'image' ? 'image' : 'pdf';
|
||||||
const navigationPath = `/master/brand-device/view/${id}/files/${fileTypeParam}/${encodedFileName}`;
|
const navigationPath = `/master/brand-device/view/${id}/files/${fileTypeParam}/${encodedFileName}`;
|
||||||
|
|
||||||
console.log('Navigating to:', navigationPath);
|
|
||||||
navigate(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 = () => {
|
const renderStepContent = () => {
|
||||||
if (currentStep === 0) {
|
if (currentStep === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -444,26 +197,140 @@ const ViewBrandDevice = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentStep === 1) {
|
if (currentStep === 1) {
|
||||||
const errorCodesCount = loading ? 3 : (brandData?.error_code?.length || 0);
|
const errorCodes = brandData?.error_code || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Title level={5} style={{ marginBottom: 16 }}>
|
<Title level={5} style={{ marginBottom: 16 }}>
|
||||||
Error Codes ({errorCodesCount})
|
Error Codes ({errorCodes.length})
|
||||||
</Title>
|
</Title>
|
||||||
{errorCodesCount > 0 ? (
|
|
||||||
<TableList
|
{errorCodes.length > 0 ? (
|
||||||
mobile={false}
|
<Collapse
|
||||||
cardColor={'#42AAFF'}
|
activeKey={activeErrorKeys}
|
||||||
header={'error_code'}
|
onChange={setActiveErrorKeys}
|
||||||
getData={getErrorCodesData}
|
style={{ marginBottom: 16 }}
|
||||||
queryParams={{}}
|
>
|
||||||
columns={errorCodeColumns}
|
{errorCodes.map((errorCode, index) => (
|
||||||
triger={errorCodesTriger}
|
<Panel
|
||||||
firstLoad={false}
|
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' }} />
|
||||||
) : (
|
) : (
|
||||||
!loading && <Text type="secondary">No error codes available</Text>
|
<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 && (
|
||||||
|
<Empty
|
||||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
|
description={
|
||||||
|
<Text type="secondary">No error codes available</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -496,9 +363,7 @@ const ViewBrandDevice = () => {
|
|||||||
<Step title="Error Codes & Solutions" />
|
<Step title="Error Codes & Solutions" />
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
{/* Content area with blur overlay during loading */}
|
|
||||||
<div style={{ position: 'relative', marginTop: 24 }}>
|
<div style={{ position: 'relative', marginTop: 24 }}>
|
||||||
{/* Overlay with blur effect during loading - only on content area */}
|
|
||||||
{loading && (
|
{loading && (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@@ -94,13 +94,15 @@ const ViewFilePage = () => {
|
|||||||
setPdfLoading(true);
|
setPdfLoading(true);
|
||||||
const folder = getFolderFromFileType('pdf');
|
const folder = getFolderFromFileType('pdf');
|
||||||
try {
|
try {
|
||||||
const response = await getFile(folder, decodedFileName);
|
const blobData = await getFile(folder, decodedFileName);
|
||||||
const blobUrl = window.URL.createObjectURL(response.data);
|
console.log('PDF blob data received:', blobData);
|
||||||
|
const blobUrl = window.URL.createObjectURL(blobData);
|
||||||
setPdfBlobUrl(blobUrl);
|
setPdfBlobUrl(blobUrl);
|
||||||
console.log('PDF blob URL created successfully:', blobUrl);
|
console.log('PDF blob URL created successfully:', blobUrl);
|
||||||
} catch (pdfError) {
|
} catch (pdfError) {
|
||||||
console.error('Error loading PDF:', 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 {
|
} finally {
|
||||||
setPdfLoading(false);
|
setPdfLoading(false);
|
||||||
}
|
}
|
||||||
@@ -194,7 +196,7 @@ const ViewFilePage = () => {
|
|||||||
const isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension);
|
const isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension);
|
||||||
const isPdf = fileExtension === 'pdf';
|
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
|
// Show placeholder when loading
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -260,7 +262,7 @@ const ViewFilePage = () => {
|
|||||||
return (
|
return (
|
||||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||||
<img
|
<img
|
||||||
src={fileUrl}
|
src={getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName)}
|
||||||
alt={actualFileName}
|
alt={actualFileName}
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
@@ -276,7 +278,7 @@ const ViewFilePage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isPdf) {
|
if (isPdf) {
|
||||||
const displayUrl = pdfBlobUrl || fileUrl;
|
const displayUrl = pdfBlobUrl || getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '75vh', width: '100%', border: '1px solid #d9d9d9', borderRadius: '8px', overflow: 'hidden' }}>
|
<div style={{ height: '75vh', width: '100%', border: '1px solid #d9d9d9', borderRadius: '8px', overflow: 'hidden' }}>
|
||||||
@@ -342,13 +344,15 @@ const ViewFilePage = () => {
|
|||||||
setPdfLoading(true);
|
setPdfLoading(true);
|
||||||
const folder = getFolderFromFileType('pdf');
|
const folder = getFolderFromFileType('pdf');
|
||||||
getFile(folder, actualFileName)
|
getFile(folder, actualFileName)
|
||||||
.then(response => {
|
.then(blobData => {
|
||||||
const blobUrl = window.URL.createObjectURL(response.data);
|
console.log('Retry PDF blob data:', blobData);
|
||||||
|
const blobUrl = window.URL.createObjectURL(blobData);
|
||||||
setPdfBlobUrl(blobUrl);
|
setPdfBlobUrl(blobUrl);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error retrying PDF load:', 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(() => {
|
.finally(() => {
|
||||||
setPdfLoading(false);
|
setPdfLoading(false);
|
||||||
@@ -370,7 +374,7 @@ const ViewFilePage = () => {
|
|||||||
<div style={{ fontSize: '16px', marginBottom: '8px' }}>Preview tidak tersedia untuk jenis file ini</div>
|
<div style={{ fontSize: '16px', marginBottom: '8px' }}>Preview tidak tersedia untuk jenis file ini</div>
|
||||||
<div style={{ color: '#666', marginBottom: '16px' }}>{actualFileName}</div>
|
<div style={{ color: '#666', marginBottom: '16px' }}>{actualFileName}</div>
|
||||||
<div style={{ marginTop: '16px' }}>
|
<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
|
Buka di Tab Baru
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
import { Form, Divider, Button, Switch, Input, ConfigProvider, Typography } from 'antd';
|
import {
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
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 { NotifAlert } from '../../../../components/Global/ToastNotif';
|
||||||
import SolutionField from './SolutionField';
|
import SolutionField from './SolutionField';
|
||||||
|
import { uploadFile, getFileUrl } from '../../../../api/file-uploads';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -24,10 +35,70 @@ const ErrorCodeForm = ({
|
|||||||
onFileView,
|
onFileView,
|
||||||
onCreateNewErrorCode,
|
onCreateNewErrorCode,
|
||||||
onResetForm,
|
onResetForm,
|
||||||
errorCodes
|
errorCodes,
|
||||||
|
errorCodeIcon,
|
||||||
|
onErrorCodeIconUpload,
|
||||||
|
onErrorCodeIconRemove,
|
||||||
}) => {
|
}) => {
|
||||||
const statusValue = Form.useWatch('status', errorCodeForm);
|
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 () => {
|
const handleAddErrorCode = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await errorCodeForm.validateFields();
|
const values = await errorCodeForm.validateFields();
|
||||||
@@ -41,7 +112,7 @@ const ErrorCodeForm = ({
|
|||||||
|
|
||||||
const solutionName = values[`solution_name_${fieldId}`];
|
const solutionName = values[`solution_name_${fieldId}`];
|
||||||
const textSolution = values[`text_solution_${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];
|
const solutionType = values[`solution_type_${fieldId}`] || solutionTypes[fieldId];
|
||||||
|
|
||||||
if (solutionType === 'text') {
|
if (solutionType === 'text') {
|
||||||
@@ -51,11 +122,12 @@ const ErrorCodeForm = ({
|
|||||||
type_solution: 'text',
|
type_solution: 'text',
|
||||||
text_solution: textSolution.trim(),
|
text_solution: textSolution.trim(),
|
||||||
path_solution: '',
|
path_solution: '',
|
||||||
is_active: solutionStatuses[fieldId] !== false
|
is_active: solutionStatuses[fieldId] !== false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (window.currentSolutionData && window.currentSolutionData[fieldId]) {
|
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);
|
solutions.push(solutionData);
|
||||||
@@ -63,15 +135,22 @@ const ErrorCodeForm = ({
|
|||||||
} else if (solutionType === 'file') {
|
} else if (solutionType === 'file') {
|
||||||
filesForSolution.forEach((file) => {
|
filesForSolution.forEach((file) => {
|
||||||
const solutionData = {
|
const solutionData = {
|
||||||
solution_name: solutionName || file.solution_name || file.name || `Solution ${fieldId}`,
|
solution_name:
|
||||||
type_solution: file.type_solution || (file.type.startsWith('image/') ? 'image' : 'pdf'),
|
solutionName ||
|
||||||
|
file.solution_name ||
|
||||||
|
file.name ||
|
||||||
|
`Solution ${fieldId}`,
|
||||||
|
type_solution:
|
||||||
|
file.type_solution ||
|
||||||
|
(file.type.startsWith('image/') ? 'image' : 'pdf'),
|
||||||
text_solution: '',
|
text_solution: '',
|
||||||
path_solution: file.uploadPath,
|
path_solution: file.uploadPath,
|
||||||
is_active: solutionStatuses[fieldId] !== false
|
is_active: solutionStatuses[fieldId] !== false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (window.currentSolutionData && window.currentSolutionData[fieldId]) {
|
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);
|
solutions.push(solutionData);
|
||||||
@@ -83,7 +162,8 @@ const ErrorCodeForm = ({
|
|||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
title: 'Perhatian',
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -92,15 +172,20 @@ const ErrorCodeForm = ({
|
|||||||
error_code: values.error_code,
|
error_code: values.error_code,
|
||||||
error_code_name: values.error_code_name,
|
error_code_name: values.error_code_name,
|
||||||
error_code_description: values.error_code_description,
|
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,
|
status: values.status === undefined ? true : values.status,
|
||||||
solution: solutions,
|
solution: solutions,
|
||||||
key: editingErrorCodeKey || `temp-${Date.now()}`
|
key: editingErrorCodeKey || `temp-${Date.now()}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
onAddErrorCode(newErrorCode);
|
onAddErrorCode(newErrorCode);
|
||||||
|
|
||||||
} catch (error) {
|
} 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({
|
errorCodeForm.setFieldsValue({
|
||||||
status: true,
|
status: true,
|
||||||
solution_status_0: true,
|
solution_status_0: true,
|
||||||
solution_type_0: 'text'
|
solution_type_0: 'text',
|
||||||
});
|
});
|
||||||
onResetForm();
|
onResetForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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 }}>
|
<Form.Item label="Status" style={{ margin: 0 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<Form.Item name="status" valuePropName="checked" noStyle>
|
<Form.Item name="status" valuePropName="checked" noStyle>
|
||||||
@@ -143,25 +235,110 @@ const ErrorCodeForm = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button icon={<PlusOutlined />} onClick={handleAddErrorCode}>
|
||||||
icon={<PlusOutlined />}
|
|
||||||
onClick={handleAddErrorCode}
|
|
||||||
>
|
|
||||||
{editingErrorCodeKey ? 'Update Error Code' : 'Tambah Error Code'}
|
{editingErrorCodeKey ? 'Update Error Code' : 'Tambah Error Code'}
|
||||||
</Button>
|
</Button>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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} />
|
<Input disabled={isErrorCodeFormReadOnly} />
|
||||||
</Form.Item>
|
</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} />
|
<Input disabled={isErrorCodeFormReadOnly} />
|
||||||
</Form.Item>
|
</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} />
|
<Input.TextArea disabled={isErrorCodeFormReadOnly} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -175,7 +352,7 @@ const ErrorCodeForm = ({
|
|||||||
solutionType={solutionTypes[fieldId]}
|
solutionType={solutionTypes[fieldId]}
|
||||||
solutionStatus={solutionStatuses[fieldId]}
|
solutionStatus={solutionStatuses[fieldId]}
|
||||||
isReadOnly={isErrorCodeFormReadOnly}
|
isReadOnly={isErrorCodeFormReadOnly}
|
||||||
fileList={fileList.filter(file => file.solutionId === fieldId)}
|
fileList={fileList.filter((file) => file.solutionId === fieldId)}
|
||||||
onRemove={() => onRemoveSolutionField(fieldId)}
|
onRemove={() => onRemoveSolutionField(fieldId)}
|
||||||
onSolutionTypeChange={(type) => onSolutionTypeChange(fieldId, type)}
|
onSolutionTypeChange={(type) => onSolutionTypeChange(fieldId, type)}
|
||||||
onSolutionStatusChange={(status) => onSolutionStatusChange(fieldId, status)}
|
onSolutionStatusChange={(status) => onSolutionStatusChange(fieldId, status)}
|
||||||
@@ -200,17 +377,13 @@ const ErrorCodeForm = ({
|
|||||||
|
|
||||||
{!isErrorCodeFormReadOnly && editingErrorCodeKey && (
|
{!isErrorCodeFormReadOnly && editingErrorCodeKey && (
|
||||||
<Form.Item style={{ textAlign: 'right', marginTop: 16 }}>
|
<Form.Item style={{ textAlign: 'right', marginTop: 16 }}>
|
||||||
<Button onClick={handleResetForm}>
|
<Button onClick={handleResetForm}>Kembali</Button>
|
||||||
Kembali
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isErrorCodeFormReadOnly && editingErrorCodeKey && (
|
{isErrorCodeFormReadOnly && editingErrorCodeKey && (
|
||||||
<Form.Item style={{ textAlign: 'right', marginTop: 16 }}>
|
<Form.Item style={{ textAlign: 'right', marginTop: 16 }}>
|
||||||
<Button onClick={handleResetForm}>
|
<Button onClick={handleResetForm}>Kembali</Button>
|
||||||
Kembali
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -18,36 +18,53 @@ const SolutionField = ({
|
|||||||
onFileUpload,
|
onFileUpload,
|
||||||
currentSolutionData,
|
currentSolutionData,
|
||||||
onFileView,
|
onFileView,
|
||||||
errorCodeForm
|
errorCodeForm,
|
||||||
}) => {
|
}) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentSolutionData && errorCodeForm) {
|
if (currentSolutionData && errorCodeForm) {
|
||||||
if (currentSolutionData.solution_name) {
|
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) {
|
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) {
|
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);
|
errorCodeForm.setFieldValue(`solution_type_${fieldId}`, formValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentSolutionData.is_active !== undefined) {
|
// Only set status if it's not already set to prevent overwriting user changes
|
||||||
errorCodeForm.setFieldValue(`solution_status_${fieldId}`, currentSolutionData.is_active);
|
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]);
|
}, [currentSolutionData, fieldId, errorCodeForm]);
|
||||||
|
|
||||||
const handleBeforeUpload = async (file) => {
|
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) {
|
if (!isAllowedType) {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: '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;
|
return Upload.LIST_IGNORE;
|
||||||
}
|
}
|
||||||
@@ -72,13 +89,13 @@ const SolutionField = ({
|
|||||||
NotifOk({
|
NotifOk({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Berhasil',
|
title: 'Berhasil',
|
||||||
message: `${file.name} berhasil diupload!`
|
message: `${file.name} berhasil diupload!`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Gagal',
|
title: 'Gagal',
|
||||||
message: `Gagal mengupload ${file.name}`
|
message: `Gagal mengupload ${file.name}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -86,7 +103,7 @@ const SolutionField = ({
|
|||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: '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,
|
padding: 16,
|
||||||
border: '1px solid #d9d9d9',
|
border: '1px solid #d9d9d9',
|
||||||
borderRadius: 8,
|
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>
|
<Text strong>Solution {index + 1}</Text>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
@@ -122,19 +146,34 @@ const SolutionField = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="Status">
|
<Form.Item label="Status">
|
||||||
|
<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' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<Switch
|
<Switch
|
||||||
checked={solutionStatus}
|
checked={currentStatus === true}
|
||||||
onChange={(checked) => {
|
onChange={(checked) => {
|
||||||
onSolutionStatusChange(fieldId, checked);
|
setFieldValue(`solution_status_${fieldId}`, checked);
|
||||||
}}
|
}}
|
||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
style={{ backgroundColor: solutionStatus ? '#23A55A' : '#bfbfbf' }}
|
style={{
|
||||||
|
backgroundColor: solutionStatus ? '#23A55A' : '#bfbfbf',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Text style={{ marginLeft: 8 }}>
|
<Text style={{ marginLeft: 8 }}>
|
||||||
{solutionStatus ? 'Active' : 'Non Active'}
|
{currentStatus === true ? 'Active' : 'Non Active'}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="Solution Type">
|
<Form.Item label="Solution Type">
|
||||||
@@ -151,12 +190,23 @@ const SolutionField = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</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 }) => {
|
{({ getFieldValue }) => {
|
||||||
const currentType = getFieldValue(`solution_type_${fieldId}`) || 'text';
|
const currentType = getFieldValue(`solution_type_${fieldId}`) || 'text';
|
||||||
const displayType = currentType === 'file' && currentSolutionData ?
|
const displayType =
|
||||||
(currentSolutionData.type_solution === 'image' ? 'image' :
|
currentType === 'file' && currentSolutionData
|
||||||
currentSolutionData.type_solution === 'pdf' ? 'pdf' : 'file') : currentType;
|
? currentSolutionData.type_solution === 'image'
|
||||||
|
? 'image'
|
||||||
|
: currentSolutionData.type_solution === 'pdf'
|
||||||
|
? 'pdf'
|
||||||
|
: 'file'
|
||||||
|
: currentType;
|
||||||
|
|
||||||
return displayType === 'text' ? (
|
return displayType === 'text' ? (
|
||||||
<Form.Item name={`text_solution_${fieldId}`} label="Text Solution">
|
<Form.Item name={`text_solution_${fieldId}`} label="Text Solution">
|
||||||
@@ -169,24 +219,48 @@ const SolutionField = ({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Show existing file info for both preview and edit mode */}
|
{/* Show existing file info for both preview and edit mode */}
|
||||||
{currentSolutionData && currentSolutionData.type_solution !== 'text' && currentSolutionData.path_solution && (
|
{currentSolutionData &&
|
||||||
|
currentSolutionData.type_solution !== 'text' &&
|
||||||
|
currentSolutionData.path_solution && (
|
||||||
<Form.Item label="Current Document">
|
<Form.Item label="Current Document">
|
||||||
{(() => {
|
{(() => {
|
||||||
const solution = currentSolutionData;
|
const solution = currentSolutionData;
|
||||||
const fileName = solution.file_upload_name || solution.path_solution?.split('/')[1] || 'File';
|
const fileName =
|
||||||
|
solution.file_upload_name ||
|
||||||
|
solution.path_solution?.split('/')[1] ||
|
||||||
|
'File';
|
||||||
const fileType = solution.type_solution;
|
const fileType = solution.type_solution;
|
||||||
|
|
||||||
if (fileType !== 'text' && solution.path_solution) {
|
if (fileType !== 'text' && solution.path_solution) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
marginBottom: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Text>
|
<Text>
|
||||||
{fileType === 'image' ? '[Image]' : '[Document]'} {fileName}
|
{fileType === 'image'
|
||||||
|
? '[Image]'
|
||||||
|
: '[Document]'}{' '}
|
||||||
|
{fileName}
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => onFileView(solution.path_solution, solution.type_solution)}
|
onClick={() =>
|
||||||
style={{ padding: 0, height: 'auto', fontSize: '12px' }}
|
onFileView(
|
||||||
|
solution.path_solution,
|
||||||
|
solution.type_solution
|
||||||
|
)
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
padding: 0,
|
||||||
|
height: 'auto',
|
||||||
|
fontSize: '12px',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
View Document
|
View Document
|
||||||
</Button>
|
</Button>
|
||||||
@@ -204,21 +278,32 @@ const SolutionField = ({
|
|||||||
accept=".pdf,.jpg,.jpeg,.png,.gif"
|
accept=".pdf,.jpg,.jpeg,.png,.gif"
|
||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
fileList={[
|
fileList={[
|
||||||
...fileList.filter(file => file.solutionId === fieldId),
|
...fileList.filter((file) => file.solutionId === fieldId),
|
||||||
// Add existing file to fileList if it exists
|
// Add existing file to fileList if it exists
|
||||||
...(currentSolutionData && currentSolutionData.type_solution !== 'text' && currentSolutionData.path_solution ? [{
|
...(currentSolutionData &&
|
||||||
|
currentSolutionData.type_solution !== 'text' &&
|
||||||
|
currentSolutionData.path_solution
|
||||||
|
? [
|
||||||
|
{
|
||||||
uid: `existing-${fieldId}`,
|
uid: `existing-${fieldId}`,
|
||||||
name: currentSolutionData.file_upload_name || currentSolutionData.path_solution?.split('/')[1] || 'File',
|
name:
|
||||||
|
currentSolutionData.file_upload_name ||
|
||||||
|
currentSolutionData.path_solution?.split(
|
||||||
|
'/'
|
||||||
|
)[1] ||
|
||||||
|
'File',
|
||||||
status: 'done',
|
status: 'done',
|
||||||
url: null, // We'll use the path_solution for viewing
|
url: null, // We'll use the path_solution for viewing
|
||||||
solutionId: fieldId,
|
solutionId: fieldId,
|
||||||
type_solution: currentSolutionData.type_solution,
|
type_solution:
|
||||||
|
currentSolutionData.type_solution,
|
||||||
uploadPath: currentSolutionData.path_solution,
|
uploadPath: currentSolutionData.path_solution,
|
||||||
existingFile: true
|
existingFile: true,
|
||||||
}] : [])
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
]}
|
]}
|
||||||
onRemove={(file) => {
|
onRemove={(file) => {}}
|
||||||
}}
|
|
||||||
beforeUpload={handleBeforeUpload}
|
beforeUpload={handleBeforeUpload}
|
||||||
>
|
>
|
||||||
<Button icon={<UploadOutlined />} disabled={isReadOnly}>
|
<Button icon={<UploadOutlined />} disabled={isReadOnly}>
|
||||||
|
|||||||
@@ -194,6 +194,9 @@ export const useErrorCodeLogic = (errorCodeForm, fileList) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSolutionStatusChange = (fieldId, status) => {
|
const handleSolutionStatusChange = (fieldId, status) => {
|
||||||
|
// Update form immediately
|
||||||
|
errorCodeForm.setFieldValue(`solution_status_${fieldId}`, status);
|
||||||
|
// Then update local state
|
||||||
setSolutionStatuses(prev => ({
|
setSolutionStatuses(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[fieldId]: status
|
[fieldId]: status
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ const DetailUnit = (props) => {
|
|||||||
<Input
|
<Input
|
||||||
name="unit_code"
|
name="unit_code"
|
||||||
value={formData.unit_code || ''}
|
value={formData.unit_code || ''}
|
||||||
placeholder="Dibuat otomatis oleh sistem"
|
placeholder="Unit Code Auto Fill"
|
||||||
disabled
|
disabled
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: '#f5f5f5',
|
||||||
|
|||||||
Reference in New Issue
Block a user