lavoce #11

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

View File

@@ -5,9 +5,18 @@ const API_BASE_URL = import.meta.env.VITE_API_SERVER;
// Get file from uploads directory // 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;
}; };

View File

@@ -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;
}; };

View File

@@ -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',

View File

@@ -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>
), ),
}, },

View File

@@ -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>
); );
}; };

View File

@@ -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>
); );

View File

@@ -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}

View File

@@ -4,6 +4,7 @@ import { Divider, Typography, Button, Steps, Form, Row, Col, Card, Spin, Modal }
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif'; import { 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>

View File

@@ -1,11 +1,10 @@
import React, { memo, useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate, useParams, useLocation } from 'react-router-dom'; import { 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',

View File

@@ -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>

View File

@@ -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>
)} )}
</> </>

View File

@@ -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}>

View File

@@ -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

View File

@@ -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',