This commit is contained in:
Yang Li 2026-01-29 21:01:01 -05:00
parent 98d86c0f70
commit c1991211aa
87 changed files with 10387 additions and 5074 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
app/.DS_Store vendored

Binary file not shown.

View File

@ -1,15 +1,18 @@
const devUri = "mongodb://localhost:27017/worldshine";
const localUri = "mongodb+srv://new-user-test:Testing123@cluster0.qkzim.mongodb.net/leapbase?retryWrites=true&w=majority";
// const localUri = "mongodb+srv://new-user-test:Testing123@cluster0.qkzim.mongodb.net/leapbase?retryWrites=true&w=majority";
const localUri = "mongodb+srv://liyang1000000:Testing123@juxing.iosxg2f.mongodb.net/worldshine?retryWrites=true&w=majority&appName=juxing";
module.exports = {
baseUrl: "mongodb://localhost:27017/",
fileUrl: "https://worldshine.mayo.llc/files/",
database: "worldshine",
url: devUri,
// baseUrl: "mongodb://localhost:27017/",
// fileUrl: "https://worldshine.mayo.llc/files/",
// database: "worldshine",
// url: devUri,
// on local enable this
// url: localUri,
url: localUri,
// baseUrl: "mongodb+srv://new-user-test:Testing123@cluster0.qkzim.mongodb.net/",
// database: "leapbase",
// fileUrl: "http://localhost:8080/files/",
// imgBucket: "photos",
baseUrl: "mongodb+srv://liyang1000000:Testing123@juxing.iosxg2f.mongodb.net/",
database: "worldshine",
fileUrl: "http://localhost:8080/files/",
imgBucket: "photos",
};

View File

@ -47,7 +47,10 @@ exports.createCalendarEvent = (req, res) => {
event_location: req.body.event_location,
event_prediction_date: req.body.event_prediction_date,
event_reminder_type: req.body.event_reminder_type,
rrule: req.body.rrule
rrule: req.body.rrule,
meal_type: req.body.meal_type,
activity_category: req.body.activity_category,
ingredients: req.body.ingredients
});
// Save event in the database
calendarEvent

View File

@ -12,16 +12,76 @@ exports.createCustomer = (req, res) => {
const site = splitSite.findSiteNumber(req);
// Create a Customer
const customer = new Customer({
// Basic Info
username: req.body.username || req.body.email || '',
name: req.body.name || '',
firstname: req.body.firstname || '',
middle_name: req.body.middle_name || '',
lastname: req.body.lastname || '',
name_cn: req.body.name_cn || '',
name_on_id: req.body.name_on_id || '',
type: req.body.type || '',
program_type: req.body.program_type || '',
pay_source: req.body.pay_source || '',
pay_source_other: req.body.pay_source_other || '',
birth_date: req.body.birth_date || null,
legal_sex: req.body.legal_sex || '',
gender: req.body.gender || req.body.legal_sex || '',
marital_status: req.body.marital_status || '',
marriage_date: req.body.marriage_date || null,
immigration_status: req.body.immigration_status || '',
immigration_status_other: req.body.immigration_status_other || '',
language_spoken: req.body.language_spoken || [],
language_spoken_other: req.body.language_spoken_other || '',
language: req.body.language || '',
// Contact Info
email: req.body.email || '',
password: req.body.password ? bcrypt.hashSync(req.body.password, 8) : '',
phone: req.body.phone || '',
mobile_phone: req.body.mobile_phone || '',
home_phone: req.body.home_phone || '',
phone: req.body.phone || '',
language: req.body.language || '',
status: 'active',
// Address 1
address_line_1: req.body.address_line_1 || '',
address_line_2: req.body.address_line_2 || '',
city: req.body.city || '',
state: req.body.state || '',
zip_code: req.body.zip_code || '',
address_note: req.body.address_note || '',
// Address 2
address2_line_1: req.body.address2_line_1 || '',
address2_line_2: req.body.address2_line_2 || '',
city2: req.body.city2 || '',
state2: req.body.state2 || '',
zip_code2: req.body.zip_code2 || '',
address2_note: req.body.address2_note || '',
// Address 3
address3_line_1: req.body.address3_line_1 || '',
address3_line_2: req.body.address3_line_2 || '',
city3: req.body.city3 || '',
state3: req.body.state3 || '',
zip_code3: req.body.zip_code3 || '',
address3_note: req.body.address3_note || '',
// Address 4
address4_line_1: req.body.address4_line_1 || '',
address4_line_2: req.body.address4_line_2 || '',
city4: req.body.city4 || '',
state4: req.body.state4 || '',
zip_code4: req.body.zip_code4 || '',
address4_note: req.body.address4_note || '',
// Address 5
address5_line_1: req.body.address5_line_1 || '',
address5_line_2: req.body.address5_line_2 || '',
city5: req.body.city5 || '',
state5: req.body.state5 || '',
zip_code5: req.body.zip_code5 || '',
address5_note: req.body.address5_note || '',
// Legacy address fields
address1: req.body.address1 || '',
address2: req.body.address2 || '',
address3: req.body.address3 || '',
@ -32,75 +92,130 @@ exports.createCustomer = (req, res) => {
state1: req.body.state1 || '',
zip_code1: req.body.zip_code1 || '',
street_address_2: req.body.street_address_2 || '',
city2: req.body.city2 || '',
state2: req.body.state2 || '',
zip_code2: req.body.zip_code2 || '',
street_address_3: req.body.street_address_3 || '',
city3: req.body.city3 || '',
state3: req.body.state3 || '',
zip_code3: req.body.zip_code3 || '',
street_address_4: req.body.street_address_4 || '',
city4: req.body.city4 || '',
state4: req.body.state4 || '',
zip_code4: req.body.zip_code4 || '',
street_address_5: req.body.street_address_5 || '',
city5: req.body.city5 || '',
state5: req.body.state5 || '',
zip_code5: req.body.zip_code5 || '',
firstname: req.body.firstname || '',
lastname: req.body.lastname || '',
birth_date: req.body.birth_date || null,
create_by: req.body.create_by || '',
create_date: new Date(),
edit_by: req.body.edit_by || '',
edit_date: new Date(),
note: req.body.note || '',
care_provider: req.body.care_provider || '',
apartment: req.body.apartment || '',
// Emergency Contact
emergency_contact: req.body.emergency_contact || '',
emergency_contact2: req.body.emergency_contact2 || '',
emergency_contact_name: req.body.emergency_contact_name || '',
emergency_contact_phone: req.body.emergency_contact_phone || '',
emergency_contact_relationship: req.body.emergency_contact_relationship || '',
emergency_contact_relationship_other: req.body.emergency_contact_relationship_other || '',
emergency_contact_role: req.body.emergency_contact_role || [],
emergency_contact2_name: req.body.emergency_contact2_name || '',
emergency_contact2_phone: req.body.emergency_contact2_phone || '',
emergency_contact2_relationship: req.body.emergency_contact2_relationship || '',
medicare_number: req.body.medicare_number || '',
medicaid_number: req.body.medicaid_number || '',
pharmacy: req.body.pharmacy || '',
type: req.body.type || '',
avatar: req.body.avatar || '',
special_needs: req.body.special_needs || '',
pickup_status: req.body.pickup_status || '',
pharmacy_id: req.body.pharmacy_id || '',
pin: req.body.pin || '',
emergency_contact2_relationship_other: req.body.emergency_contact2_relationship_other || '',
emergency_contact2_role: req.body.emergency_contact2_role || [],
// Schedule
days_of_week: req.body.days_of_week || [],
// Admission & Discharge Record
admission_date: req.body.admission_date || null,
seating: req.body.seating || '',
vehicle_no: req.body.vehicle_no || '',
caller: req.body.caller || '',
enrolled_date: req.body.enrolled_date || null,
create_by: req.body.create_by || '',
create_date: new Date(),
referral_source: req.body.referral_source || '',
referral_source_other: req.body.referral_source_other || '',
discharge_date: req.body.discharge_date || null,
placement: req.body.placement || '',
nickname: req.body.nickname || '',
discharge_by: req.body.discharge_by || '',
discharge_reason: req.body.discharge_reason || '',
discharge_reason_other: req.body.discharge_reason_other || '',
join_reason: req.body.join_reason || '',
// Care & Services
dietary_restrictions: req.body.dietary_restrictions || [],
dietary_restrictions_other: req.body.dietary_restrictions_other || '',
diet_texture: req.body.diet_texture || '',
table_id: req.body.table_id || '',
groups: req.body.groups || null,
tags: req.body.tags || null,
roles: req.body.roles || null,
apartment: req.body.apartment || '',
private_note: req.body.private_note || '',
parent_id: '5eee3552b02fac3d4acfd5ea',
site,
disability: req.body.disability || false,
weight: req.body.weight || '',
height: req.body.height || '',
gender: req.body.gender || '',
seat_number: req.body.seat_number || '',
seating: req.body.seating || '',
transportation_type: req.body.transportation_type || '',
consent_to_text_messages: req.body.consent_to_text_messages || '',
text_msg_enabled: req.body.text_msg_enabled || false,
health_condition: String,
preferred_text_language: req.body.preferred_text_language || '',
consent_to_media_use: req.body.consent_to_media_use || '',
pickup_status: req.body.pickup_status || '',
// Medical & Insurance - Providers
care_provider: req.body.care_provider || '',
primary_care_physician: req.body.primary_care_physician || '',
pharmacy: req.body.pharmacy || '',
pharmacy_id: req.body.pharmacy_id || '',
// General Conditions
diabetes_mellitus: req.body.diabetes_mellitus || '',
eyes_on: req.body.eyes_on || '',
disability: req.body.disability || false,
wheelchair: req.body.wheelchair || '',
special_needs: req.body.special_needs || '',
health_condition: req.body.health_condition || '',
allergy_info: req.body.allergy_info || '',
meal_requirement: req.body.meal_requirement || '',
service_requirement: req.body.service_requirement || '',
// Legal
molst: req.body.molst || '',
provisions_for_advance_medical: req.body.provisions_for_advance_medical || '',
hospice: req.body.hospice || '',
burial_arrangements: req.body.burial_arrangements || '',
power_of_attorney: req.body.power_of_attorney || '',
// Rounding
requires_rounding: req.body.requires_rounding || '',
rounding_notes: req.body.rounding_notes || '',
// Confidential Details
medicare_number: req.body.medicare_number || '',
medicaid_number: req.body.medicaid_number || '',
social_security_number: req.body.social_security_number || '',
adcaps_id: req.body.adcaps_id || '',
// Compliance & Deadlines
adcaps_completed_date: req.body.adcaps_completed_date || null,
center_qualification_renew_date: req.body.center_qualification_renew_date || null,
medicaid_renew_date: req.body.medicaid_renew_date || null,
id_expiration_date: req.body.id_expiration_date || null,
// Form Submission
hipaa_authorization_form: req.body.hipaa_authorization_form || '',
medication_management_consent_form: req.body.medication_management_consent_form || '',
freedom_of_choice_form: req.body.freedom_of_choice_form || '',
meal_benefit_application_form: req.body.meal_benefit_application_form || '',
photo_video_release_form: req.body.photo_video_release_form || '',
security_deposit_agreement_form: req.body.security_deposit_agreement_form || '',
recreational_program_contract_form: req.body.recreational_program_contract_form || '',
tb_form: req.body.tb_form || '',
pre_screening_form: req.body.pre_screening_form || '',
// Additional Information
note: req.body.note || '',
private_note: req.body.private_note || '',
// Other fields
password: req.body.password ? bcrypt.hashSync(req.body.password, 8) : '',
avatar: req.body.avatar || '',
parent_id: '5eee3552b02fac3d4acfd5ea',
nickname: req.body.nickname || '',
pin: req.body.pin || '',
vehicle_no: req.body.vehicle_no || '',
caller: req.body.caller || '',
placement: req.body.placement || '',
height: req.body.height || '',
weight: req.body.weight || '',
status: 'active',
roles: req.body.roles || [],
groups: req.body.groups || [],
tags: req.body.tags || [],
edit_by: req.body.edit_by || '',
edit_date: new Date(),
payment_due_date: req.body.payment_due_date || '',
payment_status: req.body.payment_status || '',
join_reason: req.body.join_reason || '',
discharge_reason: req.body.discharge_reason || ''
site
});
// Save Customer in the database
customer

View File

@ -14,26 +14,17 @@ exports.createEmployee = (req, res) => {
// Create a Employee
const employee = new Employee({
username: req.body.username || req.body.email || '',
name: req.body.name || '',
name_cn: req.body.name_cn || '',
email: req.body.email || '',
password: req.body.password ? bcrypt.hashSync(req.body.password, 8) : '',
roles: req.body.roles || [],
mobile_phone: req.body.mobile_phone || '',
phone: req.body.phone || '',
home_phone: req.body.home_phone || '',
language: req.body.language || '',
employment_status: req.body.employment_status || '',
status: req.body.status || 'active',
address: req.body.address || '',
title: req.body.title || '',
title_cn: req.body.title_cn || '',
firstname: req.body.firstname || '',
lastname: req.body.lastname || '',
department: req.body.department || '',
birth_date: req.body.birth_date || null,
date_hired: req.body.date_hired || '',
driver_capacity: req.body.driver_capacity || null,
date_hired: req.body.date_hired || null,
create_by: req.body.create_by || '',
create_date: new Date(),
edit_by: req.body.edit_by || '',
@ -41,7 +32,68 @@ exports.createEmployee = (req, res) => {
note: req.body.note || '',
tags: req.body.tags || [],
fetch_route_time: req.body.fetch_route_time || null,
site
site,
// new fields added and legacy fields are used in HR system
trinet_id: req.body.trinet_id || '',
name: req.body.name || '',
firstname: req.body.firstname || '',
middlename: req.body.middlename || '',
lastname: req.body.lastname || '',
preferred_name: req.body.preferred_name || '',
ssn: req.body.ssn || '',
marital_status: req.body.marital_status || '',
gender: req.body.gender || '',
city: req.body.city || '',
street_address: req.body.street_address || '',
state: req.body.state || '',
zip: req.body.zip || '',
country: req.body.country || '',
work_email: req.body.work_email || '',
personal_email: req.body.personal_email || '',
phone: req.body.phone || '',
work_phone: req.body.work_phone || '',
birth_date: req.body.birth_date || '',
work_phone_ext: req.body.work_phone_ext || '',
dietary_restrictions: req.body.dietary_restrictions || '',
tshirt_size: req.body.tshirt_size || '',
title: req.body.title || '',
title_cn: req.body.title_cn || '',
department: req.body.department || '',
manager_name: req.body.manager_name || '',
manager_email: req.body.manager_email || '',
manager_trinet_id: req.body.manager_trinet_id || '',
manager_id: req.body.manager_id || '',
direct_reports: req.body.direct_reports || [],
current_employment_start_date: req.body.current_employment_start_date || '',
initial_employment_start_date: req.body.initial_employment_start_date || '',
status: req.body.status || 'active',
work_location: req.body.work_location || '',
employment_type: req.body.employment_type || '',
employment_start_date: req.body.employment_start_date || '',
employment_end_date: req.body.employment_end_date || '',
termination_date: req.body.termination_date || '',
termination_type: req.body.termination_type || '',
termination_reason: req.body.termination_reason || '',
full_time_start_date: req.body.full_time_start_date || '',
end_date: req.body.end_date || '',
month_of_service: req.body.month_of_service || null,
full_years_of_service: req.body.full_years_of_service || null,
compensation_type: req.body.compensation_type || '',
compensation: req.body.compensation || null,
worker_type: req.body.worker_type || '',
monthly_salary: req.body.monthly_salary || null,
currency: req.body.currency || '',
salary: req.body.salary || null,
hourly_wage: req.body.hourly_wage || null,
work_site: req.body.work_site || null,
next_anniversary: req.body.next_anniversary || null,
job_id: req.body.job_id || '',
job_title: req.body.job_title || '',
job_category: req.body.job_category || '',
add_to_payroll: req.body.add_to_payroll || false,
collect_tax_information: req.body.collect_tax_information || false,
fingerprints: req.body.fingerprints || [],
entry_passcode: req.body.entry_passcode || []
});
// Save Employee in the database
employee
@ -64,6 +116,31 @@ exports.getAllEmployees = (req, res) => {
if (params.status) {
condition.status = params.status;
}
if (params.role) {
condition.roles = params.role;
}
if (params.site) {
condition.site = params.site;
}
Employee.find(condition)
.then(data => {
res.send(data);
})
.catch(err => {
res.status(500).send({
message:
err.message || "Some error occurred while retrieving employees."
});
});
};
// Retrive all Employees without site
exports.getAllEmployeesAcrossSites = (req, res) => {
var params = req.query;
var condition = {};
if (params.status) {
condition.status = params.status;
}
if (params.role) {
condition.roles = params.role;
}
@ -78,6 +155,7 @@ exports.getAllEmployees = (req, res) => {
});
});
};
// Retrieve all Active Employee from the database.
exports.getAllActiveEmployees = (req, res) => {
var params = req.query;
@ -86,6 +164,9 @@ exports.getAllActiveEmployees = (req, res) => {
if (params.roles) {
condition.roles = params.roles;
}
if (params.site) {
condition.site = params.site;
}
Employee.find(condition)
.then(data => {
res.send(data);

View File

@ -10,33 +10,51 @@ exports.createResource = (req, res) => {
}
const site = splitSite.findSiteNumber(req);
const resource = new Resource({
name: req.body.name,
name_original: req.body.name_original,
name_branch: req.body.name_branch,
// Basic Information
name: req.body.name, // Provider name
office_name: req.body.office_name || '',
specialty: req.body.specialty,
type: req.body.type, // value may be ['doctor', 'pharmacy' or 'other']
color: req.body.color,
address: req.body.address,
// Legacy fields for backward compatibility
name_original: req.body.name_original || req.body.office_name || '',
name_branch: req.body.name_branch || '',
// Contact Information
phone: req.body.phone, // Office Phone Number
contact: req.body.contact, // Secondary Phone Number
fax: req.body.fax,
email: req.body.email,
// Address (split fields)
address_line_1: req.body.address_line_1 || '',
address_line_2: req.body.address_line_2 || '',
city: req.body.city,
state: req.body.state,
zipcode: req.body.zipcode,
phone: req.body.phone,
status: req.body.status || 'active', // value might be ['active', 'inactive']
// Legacy address field
address: req.body.address || req.body.address_line_1 || '',
// Additional Information
note: req.body.note,
// Legacy fields
description: req.body.description || '',
color: req.body.color || '',
category: req.body.category,
// System fields
status: req.body.status || 'active',
create_by: req.body.create_by,
create_date: req.body.create_date || new Date(),
parent_id: req.body.parent_id,
ext_id: req.body.ext_id,
category: req.body.category,
description: req.body.description,
contact: req.body.contact,
fax: req.body.fax,
email: req.body.email,
note: req.body.note,
data: req.body.data,
edit_by: req.body.edit_by,
edit_date: req.body.edit_date || new Date(),
images: req.body.images,
edit_history: req.body.edit_history,
edit_history: req.body.edit_history,
site
});
// Save Resource in the database

View File

@ -10,26 +10,44 @@ exports.createVehicle = (req, res) => {
const site = splitSite.findSiteNumber(req);
// Create a Vehicle
const vehicle = new Vehicle({
// Basic Information
vehicle_number: req.body.vehicle_number,
tag: req.body.tag || '',
ezpass: req.body.ezpass || '',
gps_tag: req.body.gps_tag || '',
mileage: req.body.mileage || 0,
responsible_driver: req.body.responsible_driver || '',
responsible_driver_id: req.body.responsible_driver_id || '',
capacity: req.body.capacity || 0,
mileage: req.body.mileage || 0,
make: req.body.make || '',
vehicle_model: req.body.vehicle_model || '',
year: req.body.year || '',
checklist: req.body.checklist || '',
status: 'active',
site,
has_lift_equip: req.body.has_lift_equip,
vin: req.body.vin || '',
note: req.body.note || '',
insurance_expire_on: req.body.insurance_expire_on,
title_registration_on: req.body.title_registration_on,
emission_test_on: req.body.emission_test_on,
tag: req.body.tag || '',
gps_tag: req.body.gps_tag || '',
ezpass: req.body.ezpass || '',
has_lift_equip: req.body.has_lift_equip,
fuel_type: req.body.fuel_type || '',
title: req.body.title || '',
title_other: req.body.title_other || '',
// Compliance & Deadlines
insurance_start_date: req.body.insurance_start_date || '',
vehicle_registration_date: req.body.vehicle_registration_date || '',
// Legacy fields (keeping for backward compatibility)
insurance_expire_on: req.body.insurance_expire_on || '',
title_registration_on: req.body.title_registration_on || '',
emission_test_on: req.body.emission_test_on || '',
oil_change_mileage: req.body.oil_change_mileage,
oil_change_date: req.body.oil_change_date
oil_change_date: req.body.oil_change_date || '',
// Check List
checklist: req.body.checklist || [],
// Additional Information
note: req.body.note || '',
// System fields
status: 'active',
site
});
// Save Vehicle in the database

View File

@ -81,7 +81,12 @@ module.exports = mongoose => {
event_location: String,
event_prediction_date: String,
event_reminder_type: String,
rrule: String
rrule: String,
meal_type: String,
// meal_type could be 'breakfast', 'lunch', 'snack'
activity_category: String,
// activity_category could be 'classes', 'games', 'events', 'outings', 'personal_care', 'care_activities'
ingredients: String
},
{ collection: 'calendar_event', timestamps: true }
);

View File

@ -2,33 +2,84 @@ const uniqueValidator = require('mongoose-unique-validator');
module.exports = mongoose => {
var schema = mongoose.Schema(
{
// Basic Info
username: {
type: String,
unique: true
},
name: String,
firstname: String,
middle_name: String,
lastname: String,
name_cn: String,
name_on_id: String,
type: String,
program_type: String,
pay_source: String,
pay_source_other: String,
birth_date: String,
legal_sex: String,
gender: String, // keeping for backward compatibility
marital_status: String,
marriage_date: String,
immigration_status: String,
immigration_status_other: String,
language_spoken: [{
type: String
}],
language_spoken_other: String,
language: String, // keeping for backward compatibility
// Contact Info
email: {
type: String,
unique: true
},
parent_id: String,
password: String,
care_provider: String,
emergency_contact: String,
emergency_contact2: String,
emergency_contact_name: String,
emergency_contact_phone: String,
emergency_contact_relationship: String,
emergency_contact2_name: String,
emergency_contact2_phone: String,
emergency_contact2_relationship: String,
medicare_number: String,
medicaid_number: String,
pharmacy: String,
birth_date: String,
firstname: String,
lastname: String,
phone: String,
mobile_phone: String,
home_phone: String,
// Address 1
address_line_1: String,
address_line_2: String,
city: String,
state: String,
zip_code: String,
address_note: String,
// Address 2
address2_line_1: String,
address2_line_2: String,
city2: String,
state2: String,
zip_code2: String,
address2_note: String,
// Address 3
address3_line_1: String,
address3_line_2: String,
city3: String,
state3: String,
zip_code3: String,
address3_note: String,
// Address 4
address4_line_1: String,
address4_line_2: String,
city4: String,
state4: String,
zip_code4: String,
address4_note: String,
// Address 5
address5_line_1: String,
address5_line_2: String,
city5: String,
state5: String,
zip_code5: String,
address5_note: String,
// Legacy address fields (keeping for backward compatibility)
address1: String,
address2: String,
address3: String,
@ -39,75 +90,171 @@ module.exports = mongoose => {
state1: String,
zip_code1: String,
street_address_2: String,
city2: String,
state2: String,
zip_code2: String,
street_address_3: String,
city3: String,
state3: String,
zip_code3: String,
street_address_4: String,
city4: String,
state4: String,
zip_code4: String,
street_address_5: String,
city5: String,
state5: String,
zip_code5: String,
phone: String,
mobile_phone: String,
type: String,
avatar: String,
special_needs: String,
note: String,
language: String,
status: String,
pickup_status: String,
apartment: String,
// Emergency Contact
emergency_contact: String, // legacy field
emergency_contact2: String, // legacy field
emergency_contact_name: String,
emergency_contact_phone: String,
emergency_contact_relationship: String,
emergency_contact_relationship_other: String,
emergency_contact_role: [{
type: String
}],
emergency_contact2_name: String,
emergency_contact2_phone: String,
emergency_contact2_relationship: String,
emergency_contact2_relationship_other: String,
emergency_contact2_role: [{
type: String
}],
emergency_contact3_name: String,
emergency_contact3_phone: String,
emergency_contact3_relationship: String,
emergency_contact3_relationship_other: String,
emergency_contact3_role: [{
type: String
}],
emergency_contact4_name: String,
emergency_contact4_phone: String,
emergency_contact4_relationship: String,
emergency_contact4_relationship_other: String,
emergency_contact4_role: [{
type: String
}],
emergency_contact5_name: String,
emergency_contact5_phone: String,
emergency_contact5_relationship: String,
emergency_contact5_relationship_other: String,
emergency_contact5_role: [{
type: String
}],
// Schedule
days_of_week: [{
type: String
}],
// Admission & Discharge Record
admission_date: String,
enrolled_date: String,
create_by: String,
create_date: Date,
edit_by: String,
edit_date: Date,
password: String,
referral_source: String,
referral_source_other: String,
discharge_date: String,
discharge_by: String,
discharge_reason: String,
discharge_reason_other: String,
join_reason: String,
// Care & Services
dietary_restrictions: [{
type: String
}],
dietary_restrictions_other: String,
diet_texture: String,
table_id: String,
seat_number: String,
seating: String, // legacy field
transportation_type: String,
consent_to_text_messages: String,
text_msg_enabled: Boolean, // legacy field
preferred_text_language: String,
consent_to_media_use: String,
pickup_status: String,
// Medical & Insurance - Providers
care_provider: String,
primary_care_physician: String,
pharmacy: String,
pharmacy_id: String,
// General Conditions
diabetes_mellitus: String,
eyes_on: String,
disability: Boolean, // legacy field for eyes_on
wheelchair: String,
special_needs: String,
health_condition: String,
allergy_info: String,
meal_requirement: String,
service_requirement: String,
// Legal
molst: String,
provisions_for_advance_medical: String,
hospice: String,
burial_arrangements: String,
power_of_attorney: String,
// Rounding
requires_rounding: String,
rounding_notes: String,
// Confidential Details
medicare_number: String,
medicaid_number: String,
social_security_number: String,
adcaps_id: String,
// Compliance & Deadlines
adcaps_completed_date: String,
center_qualification_renew_date: String,
medicaid_renew_date: String,
id_expiration_date: String,
// Form Submission - Admission Forms
hipaa_authorization_form: String,
medication_management_consent_form: String,
freedom_of_choice_form: String,
meal_benefit_application_form: String,
photo_video_release_form: String,
security_deposit_agreement_form: String,
recreational_program_contract_form: String,
// Medical Forms
tb_form: String,
pre_screening_form: String,
// Additional Information
note: String,
private_note: String,
// Other legacy fields
password: String,
salt: String,
api_token: String,
avatar: String,
parent_id: String,
nickname: String,
pin: String,
admission_date: String,
home_phone: String,
seating: String,
vehicle_no: String,
caller: String,
placement: String,
height: String,
weight: String,
status: String,
roles: [{
type: String
}],
discharge_date: String,
placement: String,
nickname: String,
table_id: String,
salt: String,
groups: [{
type: String
}],
tags: [{
type: String
}],
api_token: String,
data: String,
title: String,
apartment: String,
private_note: String,
site: Number,
disability: Boolean,
height: String,
weight: String,
gender: String,
text_msg_enabled: Boolean,
health_condition: String,
allergy_info: String,
meal_requirement: String,
service_requirement: String,
edit_by: String,
edit_date: Date,
payment_due_date: String,
payment_status: String,
join_reason: String,
discharge_reason: String
payment_status: String
},
{ collection: 'customer', timestamps: true }
);
@ -118,4 +265,4 @@ module.exports = mongoose => {
});
const Customer = mongoose.model("customer", schema);
return Customer;
};
};

View File

@ -1,39 +1,98 @@
module.exports = mongoose => {
var schema = mongoose.Schema(
{
username: String,
name: String,
name_cn: String,
email: String,
password: String,
roles: [{
type: String
}],
mobile_phone: String,
phone: String,
home_phone: String,
language: String,
employment_status: String,
status: String,
address: String,
title: String,
title_cn: String,
firstname: String,
lastname: String,
department: String,
birth_date: String,
driver_capacity: Number,
date_hired: String,
create_by: String,
create_date: Date,
edit_by: String,
edit_date: Date,
note: String,
tags: [{
type: String
}],
fetch_route_time: Date,
site: Number
username: String,
name_cn: String,
email: String,
password: String,
roles: [{
type: String
}],
mobile_phone: String,
home_phone: String,
language: String,
employment_status: String,
address: String,
date_hired: String,
driver_capacity: Number,
create_by: String,
create_date: Date,
edit_by: String,
edit_date: Date,
note: String,
tags: [{
type: String
}],
fetch_route_time: Date,
site: Number,
// new fields added and legacy feilds are used in HR system
trinet_id: String,
name: String,
firstname: String,
middlename: String,
lastname: String,
preferred_name: String,
ssn: String,
marital_status: String,
gender: String,
city: String,
street_address: String,
state: String,
zip: String,
country: String,
work_email: String,
personal_email: String,
phone: String,
work_phone: String,
birth_date: String, // date of birth
work_phone_ext: String, // work phone extension
dietary_restrictions: String,
tshirt_size: String,
title: String,
title_cn: String,
department: String,
manager_name: String,
manager_email: String,
manager_trinet_id: String,
manager_id: String,
direct_reports: [{
type: String
}],
current_employment_start_date: String,
initial_employment_start_date: String,
status: String,
work_location: String,
employment_type: String,
employment_start_date: String,
employment_end_date: String,
termination_date: String,
termination_type: String,
termination_reason: String,
full_time_start_date: String,
end_date: String,
month_of_service: Number,
full_years_of_service: Number,
compensation_type: String,
compensation: Number,
worker_type: String,
monthly_salary: Number,
currency: String,
salary: Number,
hourly_wage: Number,
work_site: Number,
next_anniversary: Number,
job_id: String,
job_title: String,
job_category: String,
add_to_payroll: Boolean,
collect_tax_information: Boolean,
fingerprints: [{
type: String
}],
entry_passcode: [{
type: String
}]
},
{ collection: 'employee', timestamps: true }
);

View File

@ -32,4 +32,6 @@ db.label = require("./label.model")(mongoose);
db.seating = require("./seating.model")(mongoose);
db.attendance_note = require("./attendance-note.model")(mongoose);
db.carousel = require("./carousel.model")(mongoose);
db.fingerprint_attendance = require("./fingerprint-attendance.model")(mongoose);
db.dailyRoutesTemplate = require("./daily-routes-template.model")(mongoose);
module.exports = db;

View File

@ -5,33 +5,49 @@ module.exports = mongoose => {
});
var schema = mongoose.Schema(
{
name: String,
name_original: String,
name_branch: String,
// Basic Information
name: String, // Provider name (renamed from doctor name)
office_name: String, // Merged from name_original and name_branch
specialty: String,
type: String, // value may be ['doctor', 'pharmacy' or 'other']
color: String,
address: String,
// Legacy fields for backward compatibility
name_original: String,
name_branch: String,
// Contact Information
phone: String, // Office Phone Number
contact: String, // Secondary Phone Number
fax: String, // Fax Number
email: String,
// Address (split fields)
address_line_1: String,
address_line_2: String,
city: String,
state: String,
zipcode: String,
phone: String,
// Legacy address field
address: String,
// Additional Information
note: String, // Merged from description and note
// Legacy fields
description: String,
color: String,
status: String, // value might be ['active', 'inactive']
// System fields
create_by: String,
create_date: Date,
parent_id: String,
ext_id: String,
category: String,
description: String,
contact: String,
fax: String,
email: String,
note: String,
data: Object,
edit_by: String,
edit_date: Date,
create_by: String,
create_date: Date,
site: Number,
images: [{
type: String
@ -49,4 +65,4 @@ module.exports = mongoose => {
});
const Resource = mongoose.model("resource", schema);
return Resource;
};
};

View File

@ -2,38 +2,52 @@ const uniqueValidator = require('mongoose-unique-validator');
module.exports = mongoose => {
var schema = mongoose.Schema(
{
// Basic Information
vehicle_number: {
type: Number,
required: true,
},
responsible_driver: String,
responsible_driver_id: String,
capacity: Number,
mileage: Number,
make: String,
vehicle_model: String,
year: String,
vin: String,
tag: {
type: String,
required: true
},
ezpass: {
type: String,
},
gps_tag: {
type: String,
},
mileage: Number,
capacity: Number,
make: String,
vehicle_model: String,
year: String,
checklist: [{
type: String
}],
status: String,
site: Number,
vin: String,
gps_tag: String,
ezpass: String,
has_lift_equip: Boolean,
fuel_type: String,
title: String,
title_other: String,
// Compliance & Deadlines
insurance_start_date: String,
vehicle_registration_date: String,
// Legacy fields (keeping for backward compatibility)
insurance_expire_on: String,
title_registration_on: String,
emission_test_on: String,
oil_change_mileage: Number,
oil_change_date: String,
note: String
// Check List
checklist: [{
type: String
}],
// Additional Information
note: String,
// System fields
status: String,
site: Number
},
{ collection: 'vehicle', timestamps: true }
);
@ -45,4 +59,4 @@ module.exports = mongoose => {
schema.index({tag: 1, site:1}, {unique: true});
const Vehicle = mongoose.model("vehicle", schema);
return Vehicle;
};
};

View File

@ -10,7 +10,9 @@ module.exports = app => {
});
var router = require("express").Router();
// Retrieve all employee
router.get("/",[authJwt.verifyToken], employees.getAllEmployees);
router.get("/", employees.getAllEmployees);
// Retrive employees across sites
router.get("/all-sites", employees.getAllEmployeesAcrossSites);
// Create a new employee
router.post("/", employees.createEmployee);
// Update an employee

BIN
app/views/.DS_Store vendored

Binary file not shown.

View File

@ -1,16 +1,16 @@
{
"files": {
"main.css": "/static/css/main.2c06dda5.css",
"main.js": "/static/js/main.cddce86b.js",
"main.css": "/static/css/main.7fa0ef40.css",
"main.js": "/static/js/main.ea61a51a.js",
"static/js/787.c4e7f8f9.chunk.js": "/static/js/787.c4e7f8f9.chunk.js",
"static/media/landing.png": "/static/media/landing.d4c6072db7a67dff6a78.png",
"index.html": "/index.html",
"main.2c06dda5.css.map": "/static/css/main.2c06dda5.css.map",
"main.cddce86b.js.map": "/static/js/main.cddce86b.js.map",
"main.7fa0ef40.css.map": "/static/css/main.7fa0ef40.css.map",
"main.ea61a51a.js.map": "/static/js/main.ea61a51a.js.map",
"787.c4e7f8f9.chunk.js.map": "/static/js/787.c4e7f8f9.chunk.js.map"
},
"entrypoints": [
"static/css/main.2c06dda5.css",
"static/js/main.cddce86b.js"
"static/css/main.7fa0ef40.css",
"static/js/main.ea61a51a.js"
]
}

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"><link rel="manifest" href="/manifest.json"/><title>Worldshine Transportation</title><script defer="defer" src="/static/js/main.cddce86b.js"></script><link href="/static/css/main.2c06dda5.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"><link rel="manifest" href="/manifest.json"/><title>Worldshine Transportation</title><script defer="defer" src="/static/js/main.ea61a51a.js"></script><link href="/static/css/main.7fa0ef40.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,140 +0,0 @@
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/*!
* @kurkle/color v0.3.4
* https://github.com/kurkle/color#readme
* (c) 2024 Jukka Kurkela
* Released under the MIT License
*/
/*!
* Chart.js v4.5.0
* https://www.chartjs.org
* (c) 2025 Chart.js Contributors
* Released under the MIT License
*/
/*!
* Signature Pad v2.3.2
* https://github.com/szimek/signature_pad
*
* Copyright 2017 Szymon Nowak
* Released under the MIT license
*
* The main idea and some parts of the code (e.g. drawing variable width Bézier curve) are taken from:
* http://corner.squareup.com/2012/07/smoother-signatures.html
*
* Implementation of interpolation using cubic Bézier curves is taken from:
* http://benknowscode.wordpress.com/2012/09/14/path-interpolation-using-cubic-bezier-and-control-point-estimation-in-javascript
*
* Algorithm for approximated length of a Bézier curve is taken from:
* http://www.lemoda.net/maths/bezier-length/index.html
*
*/
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/**
* @license
* Lodash <https://lodash.com/>
* Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* use-sync-external-store-shim.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* use-sync-external-store-shim/with-selector.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* React Router v6.3.0
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
//! moment.js

File diff suppressed because one or more lines are too long

BIN
client/.DS_Store vendored

Binary file not shown.

BIN
client/src/.DS_Store vendored

Binary file not shown.

View File

@ -10,6 +10,12 @@ body {
white-space: nowrap;
}
.btn-warning {
height: 35px;
font-size: 13px;
white-space: nowrap;
}
.text-primary {
color: #0066B1;
}
@ -103,7 +109,7 @@ input {
}
.app-side-bar-container {
width: 300px;
width: 250px;
border-right: 1px solid #ccc;
height: 100vh;
}
@ -112,7 +118,7 @@ input {
padding: 16px;
border-top: 1px solid #ccc;
border-right: 1px solid #ccc;
width: 300px;
width: 250px;
position: absolute;
bottom: 0;
left: 0;
@ -135,7 +141,7 @@ input {
}
.app-side-bar-menu-container {
height: calc(100vh - 120px);
height: calc(100vh - 150px);
overflow: auto;
}
@ -166,7 +172,7 @@ input {
.app-side-bar-logo {
padding: 16px;
border-bottom: 1px solid #ccc;
width: 300px;
width: 250px;
}
.app-layout .collapsed {
@ -180,6 +186,8 @@ input {
}
.app-main-container {
flex: 1;
min-width: 0;
padding: 40px;
height: 100vh;
overflow: auto;
@ -198,6 +206,14 @@ input {
padding: 4px 24px;
}
/* List pages: container yields to children width/height, no scrolling (browser-level scrolling) */
.app-main-content-list-container.list-page {
max-width: none;
max-height: none;
width: fit-content;
min-width: 100%;
}
.app-main-content-list-func-container {
position: relative;
}
@ -227,10 +243,18 @@ input {
color: #0066B1 !important;
}
.app-main-content-list-func-container .tab-pane {
/* List pages: tab-pane should not scroll, let browser handle it */
.app-main-content-list-container.list-page .tab-pane {
border-top: 1px solid #ccc;
padding-top: 24px;
overflow: auto;
overflow: visible;
}
/* Form pages: tab-pane should not scroll either */
.app-main-content-list-container.form-page .tab-pane {
border-top: 1px solid #ccc;
padding-top: 24px;
overflow: visible;
}
.app-main-content-list-func-container .list-func-panel {
@ -358,13 +382,20 @@ table tr {
border: 1px solid white;
}
table {
width: 100%;
table-layout: auto;
}
table tr th {
border: 1px solid white;
padding: 8px 16px;
font-size: 12px;
background-color: #0066B1;
color: white;
min-width: 250px;
width: fit-content;
max-width: 250px;
overflow-wrap: break-word;
}
table tr td {
@ -372,7 +403,9 @@ table tr td {
padding: 8px 16px;
font-size: 12px;
color: #333;
min-width: 250px;
width: fit-content;
max-width: 250px;
overflow-wrap: break-word;
}
.clickable {
@ -381,13 +414,13 @@ table tr td {
.td-checkbox, .th-checkbox {
width: 30px !important;
padding-left: 8px !important;
padding-left: 14px !important;
min-width: 5px !important;
}
.td-index, .th-index {
width: 30px !important;
padding-left: 8px !important;
padding-left: 14px !important;
min-width: 5px !important;
}
@ -413,11 +446,20 @@ table .group td {
transform: translate(0px, 37px)!important;
}
/* Form pages: container should be 100% width and height, not scrollable */
.app-main-content-list-container.form-page {
width: 100%;
max-width: none;
min-height: calc(100vh - 140px);
overflow: visible;
}
.app-main-content-fields-section {
display: flex;
align-items: baseline;
margin-top: 24px;
margin-bottom: 40px;
flex-wrap: wrap;
}
.app-main-content-fields-section.flex-end {
@ -425,6 +467,7 @@ table .group td {
align-items: flex-end;
margin-top: 24px;
margin-bottom: 40px;
flex-wrap: wrap;
}
.app-main-content-fields-section.column {
@ -456,6 +499,7 @@ table .group td {
color: black;
font-weight: 500;
line-height: 2;
white-space: nowrap;
}
.multi-columns-container {
@ -497,12 +541,15 @@ table .group td {
.app-main-content-fields-section.short .field-body {
margin-right: 20px;
min-width: 140px;
flex: 1;
}
.app-main-content-fields-section.short {
margin-bottom: 0;
margin-top: 12px;
justify-content: space-between;
display: flex;
gap: 16px;
}
.app-main-content-fields-section.with-function .react-datepicker-wrapper {
@ -516,21 +563,21 @@ table .group td {
.app-main-content-fields-section.dropdown-container input[type=text] {
width: 210px;
height: 35px;
height: 45px;
}
.app-main-content-fields-section.dropdown-container input[type=number] {
width: 210px;
height: 35px;
height: 45px;
}
.app-main-content-fields-section.dropdown-container select {
width: 210px;
height: 35px;
height: 45px;
}
.app-main-content-fields-section.with-function .react-datepicker-wrapper input[type=text] {
height: 35px;
height: 45px;
width: 120px;
}
@ -601,8 +648,9 @@ table .group td {
select {
margin-left: 5px;
min-width: 200px;
height: 40px;
height: 45px;
padding: 5px;
box-sizing: border-box;
}
a {
@ -611,11 +659,12 @@ a {
}
input[type="text"] {
height: 30px;
height: 45px;
padding-left: 15px;
padding-right: 15px;
border-radius: 8px;
border: 1px solid #ccc;
box-sizing: border-box;
}
textarea {
@ -628,33 +677,54 @@ textarea {
}
input[type="number"] {
height: 30px;
height: 45px;
padding-left: 15px;
padding-right: 15px;
border-radius: 8px;
border: 1px solid #ccc;
/* box-sizing: border-box; */
}
/* Exclude react-time-picker inputs from the padding rules above */
.react-time-picker__inputGroup__input {
padding-left: initial;
padding-right: initial;
}
input[type="email"] {
height: 30px;
height: 45px;
padding-left: 15px;
padding-right: 15px;
border-radius: 8px;
border: 1px solid #ccc;
/* box-sizing: border-box; */
}
input[type="password"] {
height: 30px;
height: 45px;
padding-left: 15px;
padding-right: 15px;
border-radius: 8px;
border: 1px solid #ccc;
/* box-sizing: border-box; */
}
input[type="checkbox"] {
margin-left: 8px;
}
/* Search input with magnifier icon */
.with-search-icon {
height: 35px !important;
padding-left: 40px !important;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23666' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: 12px center;
background-size: 18px 18px;
min-width: 200px;
}
.clickable {
cursor: pointer;
}
@ -702,6 +772,32 @@ input[type="checkbox"] {
.event-list-item-container {
border-radius: 8px;
min-width: 280px;
}
/* Center Calendar small modal styling */
.calendar-event-modal {
max-width: 380px !important;
}
.calendar-event-modal .modal-content {
width: 380px !important;
}
.calendar-event-modal .form-control,
.calendar-event-modal select.form-control,
.calendar-event-modal input.form-control {
width: 100% !important;
font-size: 13px;
}
.calendar-event-modal .modal-body {
padding: 16px 20px;
}
.calendar-event-modal .field-label {
font-size: 13px;
margin-bottom: 4px;
}
.event-red {
@ -764,6 +860,44 @@ input[type="checkbox"] {
background-color: #FFD1DC !important;
}
/* Calendar event title styles - auto-truncate based on container width */
.sx__month-grid-event .sx__month-grid-event__title,
.sx__week-grid__event .sx__event-title,
.sx__day-grid__event .sx__event-title,
.sx__month-agenda-event .sx__month-agenda-event__title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
/* Ensure calendar events show full name on hover */
.sx__month-grid-event,
.sx__week-grid__event,
.sx__day-grid__event,
.sx__month-agenda-event {
cursor: pointer;
}
/* Calendar event height - adjust to content, no cropping */
.sx__month-grid-event {
height: auto !important;
min-height: 20px;
overflow: visible !important;
white-space: normal !important;
}
.sx__month-grid-event .sx__month-grid-event__title {
white-space: normal !important;
overflow: visible !important;
text-overflow: clip !important;
line-height: 1.3;
}
.sx__month-grid-day__events {
overflow: visible !important;
}
.btn-no-deco {
text-decoration: none!important;
padding-top: 6px;
@ -994,7 +1128,7 @@ input[type="checkbox"] {
font-size: 13px;
cursor: move;
background: #eee;
margin-left: 60px;
margin-left: 65px;
}
.customers-dnd-item-container-absent {
@ -1009,8 +1143,9 @@ input[type="checkbox"] {
.new-customers-dnd-item-container {
border: none;
padding: 0 16px;
padding: 0 16px 0 25px;
margin-bottom: 24px;
margin-top: 16px;
display: flex;
align-items: center;
font-size: 13px;
@ -1024,6 +1159,7 @@ input[type="checkbox"] {
.stop-index {
position: absolute;
left: 0;
padding-right: 12px;
}
.customer-dnd-item {
@ -1053,10 +1189,44 @@ input[type="checkbox"] {
}
.react-datepicker__input-container input {
height: 35px;
height: 45px;
width: 100%;
box-sizing: border-box;
}
/* DatePicker blue theme styling */
.react-datepicker__header {
background-color: #0066B1 !important;
border-bottom: none !important;
}
.react-datepicker__current-month,
.react-datepicker__day-name,
.react-datepicker-time__header {
color: white !important;
}
.react-datepicker__day--selected,
.react-datepicker__day--keyboard-selected,
.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected {
background-color: #0066B1 !important;
color: white !important;
}
.react-datepicker__day:hover {
background-color: #EAF3FF !important;
}
.react-datepicker__navigation-icon::before {
border-color: white !important;
}
.react-datepicker__triangle::before,
.react-datepicker__triangle::after {
border-bottom-color: #0066B1 !important;
}
.admin-nav a {
text-decoration: none !important;
}
@ -1413,6 +1583,16 @@ input[type="checkbox"] {
text-align: center;
}
.seat-circle.has-customer {
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.seat-circle.has-customer:hover {
transform: scale(1.1);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.guest-name {
text-align: center;
font-size: 10px;
@ -1593,66 +1773,199 @@ input[type="checkbox"] {
width: 100vw;
height: 100vh;
z-index: 9998;
overflow: auto;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.fullscreen-mode .app-main-content-list-container {
border: none;
padding: 20px;
min-height: 100vh;
width: 100%;
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: center;
flex-direction: column;
}
.fullscreen-mode .app-main-content-list-func-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
margin-top: auto;
flex-direction: column;
padding: clamp(10px, 2vw, 30px);
box-sizing: border-box;
}
.fullscreen-mode .multi-columns-container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
height: 100%;
display: flex;
justify-content: center;
gap: 20px;
gap: clamp(10px, 1.5vw, 25px);
flex: 1;
min-height: 0;
}
.fullscreen-mode .column-container {
flex: 1;
max-width: 400px;
display: flex;
flex-direction: column;
min-width: 0;
height: 100%;
gap: clamp(8px, 1vw, 16px);
}
.fullscreen-mode .column-card {
background-color: rgba(255, 255, 255, 0.8) !important;
backdrop-filter: blur(5px);
margin-bottom: 20px;
background-color: rgba(255, 255, 255, 0.9) !important;
backdrop-filter: blur(8px);
border-radius: clamp(6px, 0.8vw, 12px);
padding: clamp(10px, 1.5vw, 24px);
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.fullscreen-mode .card {
background-color: rgba(255, 255, 255, 0.8) !important;
backdrop-filter: blur(5px);
background-color: rgba(255, 255, 255, 0.9) !important;
backdrop-filter: blur(8px);
flex: 1;
display: flex;
flex-direction: column;
}
.fullscreen-mode .card-body {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
/* Dynamic font sizing for fullscreen mode */
.fullscreen-mode h4 {
font-size: clamp(16px, 2vw, 28px);
}
.fullscreen-mode h5 {
font-size: clamp(14px, 1.8vw, 24px);
}
.fullscreen-mode h6 {
font-size: clamp(12px, 1.4vw, 20px);
margin-bottom: clamp(8px, 1vw, 16px) !important;
}
.fullscreen-mode p,
.fullscreen-mode span,
.fullscreen-mode div {
font-size: clamp(10px, 1.1vw, 16px);
}
.fullscreen-mode .text-primary {
font-size: clamp(16px, 2vw, 32px) !important;
}
/* Tables in fullscreen - fill available space */
.fullscreen-mode .info-screen-appointments-table {
width: 100%;
background-color: rgba(255, 255, 255, 0.95);
table-layout: fixed;
flex: 1;
}
.fullscreen-mode .info-screen-appointments-table th,
.fullscreen-mode .info-screen-appointments-table td {
font-size: clamp(10px, 1.1vw, 16px);
padding: clamp(6px, 0.8vw, 14px) clamp(8px, 1vw, 16px);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fullscreen-mode .info-screen-appointments-table th {
font-weight: 600;
background-color: rgba(0, 102, 177, 0.1);
}
.fullscreen-mode .list.row {
flex: 1;
display: flex;
flex-direction: column;
margin-bottom: 0 !important;
}
.fullscreen-mode .list.row .col-md-12 {
flex: 1;
display: flex;
flex-direction: column;
}
.fullscreen-mode .app-main-content-fields-section {
flex: 1;
display: flex;
flex-direction: column;
}
/* Clock and weather cards - equal sizing */
.fullscreen-mode .row.mb-4 {
display: flex;
gap: clamp(8px, 1vw, 16px);
margin-bottom: clamp(10px, 1.5vw, 24px) !important;
}
.fullscreen-mode .col-md-6 {
flex: 1;
max-width: none;
width: auto;
}
/* Clock scaling */
.fullscreen-mode .clock-container {
transform: scale(clamp(0.6, 0.08vw + 0.5, 1.2));
transform-origin: center;
}
.fullscreen-mode .clock {
width: clamp(60px, 8vw, 120px);
height: clamp(60px, 8vw, 120px);
}
/* Weather icon scaling */
.fullscreen-mode .weather-icon i {
font-size: clamp(32px, 4vw, 64px) !important;
}
/* Gallery/Carousel scaling */
.fullscreen-mode .carousel {
height: clamp(120px, 15vh, 250px) !important;
}
.fullscreen-mode .carousel-item img {
height: clamp(120px, 15vh, 250px) !important;
}
/* Attendance note card adjustments */
.fullscreen-mode .mb-4:has(.card) {
flex: 0 0 auto;
}
.fullscreen-mode .img-fluid {
max-height: clamp(80px, 10vh, 150px) !important;
}
/* Logo scaling */
.fullscreen-mode .info-logo,
.fullscreen-mode img[alt="Worldshine Logo"] {
height: clamp(20px, 2.5vw, 40px) !important;
}
.fullscreen-mode .logo-worldshine {
font-size: clamp(12px, 1.4vw, 20px) !important;
}
.fullscreen-hint {
position: fixed;
top: 20px;
right: 20px;
background-color: rgba(0, 0, 0, 0.7);
top: clamp(10px, 1.5vw, 25px);
right: clamp(10px, 1.5vw, 25px);
background-color: rgba(0, 0, 0, 0.75);
color: white;
padding: 10px 15px;
border-radius: 5px;
padding: clamp(8px, 1vw, 15px) clamp(12px, 1.5vw, 20px);
border-radius: clamp(4px, 0.5vw, 8px);
z-index: 9999;
font-size: 14px;
font-size: clamp(11px, 1.2vw, 16px);
animation: fadeIn 0.3s ease-in;
backdrop-filter: blur(4px);
}
@keyframes fadeIn {
@ -1674,39 +1987,125 @@ input[type="checkbox"] {
background-attachment: fixed;
}
/* Info Screen Table Overrides for Full Screen */
.fullscreen-mode .info-screen-appointments-table {
background-color: rgba(255, 255, 255, 0.9);
/* Prevent wrapping in fullscreen table cells */
.fullscreen-mode .personnel-info-table {
width: 100%;
}
.fullscreen-mode .personnel-info-table tbody {
display: block;
max-height: calc(100% - 40px);
overflow-y: auto;
overflow-x: hidden;
}
.fullscreen-mode .personnel-info-table thead,
.fullscreen-mode .personnel-info-table tbody tr {
display: table;
width: 100%;
table-layout: fixed;
}
.fullscreen-mode .personnel-info-table thead {
width: calc(100% - 8px); /* Account for scrollbar */
}
/* Responsive adjustments for smaller screens in fullscreen */
@media (max-width: 1400px) {
.fullscreen-mode .multi-columns-container {
gap: clamp(8px, 1vw, 15px);
}
}
/* Responsive adjustments for full screen mode */
@media (max-width: 1200px) {
.fullscreen-mode .multi-columns-container {
max-width: 1000px;
.fullscreen-mode .column-container:nth-child(2) .row.mb-4 {
flex-direction: column;
gap: 15px;
}
.fullscreen-mode .column-container {
max-width: 100%;
width: 100%;
.fullscreen-mode .carousel {
height: clamp(100px, 12vh, 180px) !important;
}
.fullscreen-mode .carousel-item img {
height: clamp(100px, 12vh, 180px) !important;
}
}
@media (max-width: 768px) {
.fullscreen-mode .app-main-content-list-container {
padding: 10px;
@media (max-height: 700px) {
.fullscreen-mode .carousel {
height: clamp(80px, 10vh, 140px) !important;
}
.fullscreen-mode .multi-columns-container {
gap: 10px;
.fullscreen-mode .carousel-item img {
height: clamp(80px, 10vh, 140px) !important;
}
.fullscreen-mode .column-card {
margin-bottom: 15px;
.fullscreen-mode h6 {
margin-bottom: clamp(4px, 0.5vw, 10px) !important;
}
}
/* Additional fullscreen utility classes */
.fullscreen-mode .fullscreen-title {
font-weight: 600;
margin-bottom: clamp(8px, 1vw, 16px);
}
.fullscreen-mode .time-weather-row {
flex: 0 0 auto;
min-height: 0;
}
.fullscreen-mode .attendance-note-wrapper,
.fullscreen-mode .gallery-wrapper {
flex: 0 0 auto;
margin-bottom: clamp(8px, 1vw, 16px);
}
.fullscreen-mode .fullscreen-date-display {
margin-bottom: clamp(4px, 0.5vw, 12px);
}
.fullscreen-mode .fullscreen-time {
font-weight: 600;
}
.fullscreen-mode .fullscreen-logo {
height: clamp(20px, 2.5vw, 35px);
margin-right: clamp(6px, 0.8vw, 12px);
}
.fullscreen-mode .fullscreen-intro {
line-height: 1.4;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
}
.fullscreen-mode .fullscreen-note-img {
width: 100%;
object-fit: cover;
border-radius: clamp(4px, 0.5vw, 8px);
}
.fullscreen-mode .fullscreen-placeholder {
height: clamp(60px, 8vh, 120px);
background-color: rgba(248, 249, 250, 0.8);
border: 2px dashed #dee2e6;
border-radius: clamp(4px, 0.5vw, 8px);
}
.fullscreen-mode .fullscreen-carousel {
border-radius: clamp(4px, 0.5vw, 8px);
overflow: hidden;
}
.fullscreen-mode .fullscreen-carousel-img {
object-fit: cover;
border-radius: clamp(4px, 0.5vw, 8px);
}
/* InfoScreen custom classes for moving inline styles to CSS */
.info-title {

View File

@ -25,6 +25,12 @@ import Login from "./components/login/Login";
import RoutesHistory from "./components/trans-routes/RoutesHistory";
import RouteTemplatesList from "./components/trans-routes/RouteTemplatesList";
import RouteTemplateEdit from "./components/trans-routes/RouteTemplateEdit";
import DailyTemplatesList from "./components/trans-routes/DailyTemplatesList";
import ViewDailyTemplate from "./components/trans-routes/ViewDailyTemplate";
import ViewDailyTemplateRoute from "./components/trans-routes/ViewDailyTemplateRoute";
import UpdateDailyTemplateRoute from "./components/trans-routes/UpdateDailyTemplateRoute";
import CreateTemplateRoute from "./components/trans-routes/CreateTemplateRoute";
import MealStatus from "./components/trans-routes/MealStatus";
import CreateCustomer from "./components/customers/CreateCustomer";
import CustomersList from "./components/customers/CustomersList";
import ViewCustomer from "./components/customers/ViewCustomer";
@ -119,6 +125,11 @@ function App() {
<Route path="history" element={<RoutesHistory />} />
<Route path="templates" element={<RouteTemplatesList />} />
<Route path="templates/edit/:id" element={<RouteTemplateEdit />} />
<Route path="daily-templates/list" element={<DailyTemplatesList />} />
<Route path="daily-templates/view/:id" element={<ViewDailyTemplate />} />
<Route path="daily-templates/:id/view-route/:routeId" element={<ViewDailyTemplateRoute />} />
<Route path="daily-templates/:id/update-route/:routeId" element={<UpdateDailyTemplateRoute />} />
<Route path="daily-templates/:id/create-route" element={<CreateTemplateRoute />} />
<Route path="route-signature" element={<RouteSignatureList/>} />
<Route path="route-report-with-signature/:id" element={<RouteReportWithSignature/>} />
</Route>
@ -158,6 +169,7 @@ function App() {
<Route path="/seating" element={<Seating />} />
<Route path="/center-calendar" element={<CenterCalendar/>} />
<Route path="/meal-status" element={<MealStatus />} />
<Route path="/medical" element={<Medical />}>
<Route path="" element={<Navigate replace to="index" />} />

View File

@ -646,7 +646,7 @@ const AdminView = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Dashboard</Breadcrumb.Item>
<Breadcrumb.Item href="/">Dashboard</Breadcrumb.Item>
<Breadcrumb.Item active>
Admin View
</Breadcrumb.Item>

View File

@ -660,7 +660,7 @@ const CustomerReport = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Transportation</Breadcrumb.Item>
<Breadcrumb.Item href="/trans-routes/dashboard">Transportation</Breadcrumb.Item>
<Breadcrumb.Item active>
Customer Reports
</Breadcrumb.Item>

View File

@ -17,6 +17,7 @@ import '@schedule-x/theme-default/dist/calendar.css';
import { Archive, PencilSquare, Filter } from "react-bootstrap-icons";
import DatePicker from "react-datepicker";
import { vehicleSlice } from "../../store";
import Select from 'react-select';
// import { Scheduler } from "@aldabil/react-scheduler";
@ -40,6 +41,8 @@ const EventsCalendar = () => {
const eventModalService = createEventModalPlugin();
const eventRecurrence = createEventRecurrencePlugin();
const [groupedEvents, setGroupedEvents] = useState(new Map());
const [currentRangeStart, setCurrentRangeStart] = useState(null);
const [currentRangeEnd, setCurrentRangeEnd] = useState(null);
const [showCreationModal, setShowCreationModal] = useState(false);
const [newEventStartDateTime, setNewEventStartDateTime] = useState(new Date());
const [newEventEndDateTime, setNewEventEndDateTime] = useState(new Date())
@ -58,6 +61,65 @@ const EventsCalendar = () => {
const [newEventRecurring, setNewEventRecurring] = useState(undefined);
const [vehicles, setVehicles] = useState([]);
const [employees, setEmployees] = useState([]);
// Medical appointment specific fields
const [newEventCustomer, setNewEventCustomer] = useState('');
const [newEventResource, setNewEventResource] = useState('');
const [newEventInterpreter, setNewEventInterpreter] = useState('');
const [newEventFasting, setNewEventFasting] = useState('');
const [newEventNeedId, setNewEventNeedId] = useState('');
const [newEventNewPatient, setNewEventNewPatient] = useState('');
const [newEventDisability, setNewEventDisability] = useState('');
const [newEventTransMethod, setNewEventTransMethod] = useState('');
// Activity specific fields
const [newActivityCategory, setNewActivityCategory] = useState('');
// Attendance Note specific fields
const [newAttendanceCustomer, setNewAttendanceCustomer] = useState(null);
const [newAttendanceReason, setNewAttendanceReason] = useState('');
// Meal Plan specific fields
const [newMealType, setNewMealType] = useState('');
const [newMealIngredients, setNewMealIngredients] = useState('');
// Important Dates specific fields
const [newReminderTitleCategory, setNewReminderTitleCategory] = useState('');
const [newReminderAssociatedEntity, setNewReminderAssociatedEntity] = useState(null);
// Helper function to format name from "lastname, firstname" to "firstname lastname"
const formatFullName = (name) => {
if (!name) return '';
if (name.includes(',')) {
const parts = name.split(',').map(part => part.trim());
return `${parts[1]} ${parts[0]}`; // firstname lastname
}
return name;
};
// Helper function to get shortened name
const getShortenedName = (name) => {
if (!name) return '';
const fullName = formatFullName(name);
const parts = fullName.split(' ');
if (parts.length >= 2) {
return `${parts[0]} ${parts[parts.length - 1].charAt(0)}`; // FirstName L
}
return fullName;
};
// Helper function to format event title - now uses full name, CSS handles overflow
const formatEventTitle = (customerName, startTime) => {
const fullName = formatFullName(customerName);
// Return full name - CSS will handle truncation with text-overflow: ellipsis
return fullName;
};
// Get full name for description/tooltip
const getEventDescription = (customerName, doctorName) => {
const fullName = formatFullName(customerName);
return doctorName ? `${fullName} - ${doctorName}` : fullName;
};
const eventTypeMap = {
medicalCalendar: 'medical',
@ -81,34 +143,57 @@ const EventsCalendar = () => {
events: events,
plugins: [eventModalService, eventsServicePlugin, eventRecurrence],
callbacks: {
onSelectedDateUpdate(date) {
setFromDate(new Date(new Date(date).getFullYear(), new Date(date).getMonth(), 1));
setToDate(new Date(new Date(date).getFullYear(), new Date(date).getMonth() + 1, 0));
onRangeUpdate(range) {
console.log('new calendar range start date', range.start);
console.log('new calendar range end date', range.end);
setCurrentRangeStart(range.start);
setCurrentRangeEnd(range.end);
// Update fromDate/toDate for API fetching based on the range
const startDate = new Date(range.start);
const endDate = new Date(range.end);
setFromDate(new Date(startDate.getFullYear(), startDate.getMonth(), 1));
setToDate(new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0));
},
onClickDate(date) {
setNewEventStartDateTime(new Date(date))
setNewEventEndDateTime(new Date(date));
setShowCreationModal(true);
},
onClickDateTime(dateTime) {
setNewEventStartDateTime(new Date(dateTime.replace(' ', 'T')));
setNewEventEndDateTime(new Date(dateTime.replace(' ', 'T')));
setShowCreationModal(true);
}
}
});
// Filter events based on current calendar range
const getFilteredEvents = () => {
console.log("CenterCalendar - range:", currentRangeStart, "to", currentRangeEnd, "events:", events.length);
if (!currentRangeStart || !currentRangeEnd) {
// If no range set yet, show all events for current month
const now = moment();
return events.filter(event => {
const eventDate = moment(event.start_time);
return eventDate.isSame(now, 'month');
});
}
// Filter events within the calendar's visible range
const rangeStart = moment(currentRangeStart);
const rangeEnd = moment(currentRangeEnd);
return events.filter(event => {
const eventDate = moment(event.start_time);
return eventDate.isBetween(rangeStart, rangeEnd, 'day', '[]');
});
};
const getGroupedEvents = () => {
const eventsDateMap = new Map();
for (const eventItem of events) {
const filteredEvents = getFilteredEvents();
for (const eventItem of filteredEvents) {
const dateString = moment(eventItem.start_time).format('MMM Do, YYYY');
if (eventsDateMap.has(dateString)) {
eventsDateMap.set(dateString, [...eventsDateMap.get(dateString), eventItem]);
@ -134,7 +219,7 @@ const EventsCalendar = () => {
EmployeeService.getAllEmployees().then((data) => {
setEmployees(data.data);
});
CustomerService.getAllCustomers().then((data) => {
CustomerService.getAllActiveCustomers().then((data) => {
setCustomers(data.data);
});
ResourceService.getAll().then((data) => {
@ -171,7 +256,8 @@ const EventsCalendar = () => {
item.endTime = item?.start_time? `${moment(new Date(item?.end_time)).format('YYYY-MM-DD HH:mm')}` : '' ;
item.fasting = item?.data?.fasting || '';
item.transportation = item?.link_event_name || '';
item.title = currentTab==='medicalCalendar' ? `${customerField}, provider: ${doctorField}`: item.title;
item.title = currentTab==='medicalCalendar' ? formatEventTitle(customerField, item?.start_time) : item.title;
item.description = currentTab==='medicalCalendar' ? getEventDescription(customerField, doctorField) : item.description; // Full info for tooltip
item.start = item?.start_time? `${moment(new Date(item?.start_time)).format('YYYY-MM-DD HH:mm')}` : `${moment().format('YYYY-MM-DD HH:mm')}`;
item.end = item?.stop_time? `${moment(new Date(item?.stop_time)).format('YYYY-MM-DD HH:mm')}` : (item?.start_time? `${moment(item?.start_time).format('YYYY-MM-DD HH:mm')}` : `${moment().format('YYYY-MM-DD HH:mm')}`);
const transportationInfo = EventsService.getTransportationInfo(allEvents, item, timeData);
@ -203,12 +289,15 @@ const EventsCalendar = () => {
}
}, [customers, resources, timeData, currentTab, allEvents, showDeletedItems])
useEffect(() => {
if (events && calendar) {
console.log("CenterCalendar useEffect - events:", events.length, "range:", currentRangeStart, "to", currentRangeEnd);
calendar?.eventsService?.set(events);
setGroupedEvents(getGroupedEvents());
}
}, [events]);
}, [events, currentRangeStart, currentRangeEnd]);
const redirectToAdmin = () => {
@ -303,13 +392,23 @@ const EventsCalendar = () => {
const customComponents = {
eventModal: ({calendarEvent}) => {
return <>
<div className="sx__event-modal__title">{currentTab === 'medicalCalendar' ? calendarEvent?.customer : calendarEvent?.title}</div>
{ calendarEvent?.doctor && <div className="sx__event-modal__time">{`${calendarEvent?.doctor}`}</div>}
{calendarEvent?.doctor && <div className="sx__event-modal__time">{`${calendarEvent?.doctor}`}</div>}
<div className="sx__event-modal__time">{`${calendarEvent?.start}`}</div>
<div className="sx__event-modal__time">
{currentTab === 'medicalCalendar' && <PencilSquare size={16} onClick={() => goToEdit(calendarEvent?.id)} className="me-4"></PencilSquare>}
<Archive size={16} onClick={() =>{disableEvent(calendarEvent?.id)}}></Archive> </div>
<div className="sx__event-modal__time" style={{ display: 'flex', gap: '12px', marginTop: '8px' }}>
<PencilSquare
size={16}
onClick={() => currentTab === 'medicalCalendar' ? goToEdit(calendarEvent?.id) : goToEdit(calendarEvent?.id)}
style={{ cursor: 'pointer' }}
title="Edit"
/>
<Archive
size={16}
onClick={() => disableEvent(calendarEvent?.id)}
style={{ cursor: 'pointer' }}
title="Delete"
/>
</div>
</>
}
};
@ -341,29 +440,126 @@ const EventsCalendar = () => {
},
);
// Get legend options based on current tab
const getLegendOptions = () => {
switch (currentTab) {
case 'medicalCalendar':
return EventsService.labelOptions;
case 'activitiesCalendar':
return EventsService.activityColorOptions;
case 'incidentsCalendar':
return EventsService.incidentColorOptions;
case 'mealPlanCalendar':
return EventsService.mealPlanColorOptions;
case 'reminderDatesCalendar':
return EventsService.colorOptions;
default:
return [];
}
};
const calendarView = <div className="multi-columns-container">
<div className="column-container" style={{'minWidth': '1000px'}}>
{calendar && <ScheduleXCalendar customComponents={customComponents} calendarApp={calendar} />}
{/* Dynamic Legend */}
<div className="calendar-legend mt-3">
<h6 className="text-muted mb-2" style={{ fontSize: '12px' }}>Legend:</h6>
<div className="d-flex flex-wrap gap-3">
{getLegendOptions().map((item) => (
<div key={item.value} className="d-flex align-items-center">
<span
className={`event-${item.value}`}
style={{
width: '16px',
height: '16px',
borderRadius: '4px',
display: 'inline-block',
marginRight: '6px'
}}
></span>
<span style={{ fontSize: '12px' }}>{item.label}</span>
</div>
))}
</div>
</div>
</div>
<div className="column-container">
<div className="column-card">
<h6 className="text-primary me-4">List</h6>
<div className="column-card" style={{ maxHeight: '800px', overflowY: 'auto', overflowX: 'hidden', padding: '16px' }}>
<h6 className="text-primary mb-3">List</h6>
{(!groupedEvents || groupedEvents.size === 0) && (
<div style={{ padding: '24px', textAlign: 'center', color: '#999' }}>
No events for this period
</div>
)}
{
Array.from(groupedEvents?.keys())?.map((key) => {
return <>
return <div key={key}>
<h6 className="text-primary me-2">{key}</h6>
{
groupedEvents.get(key).map(eventItem => <div className={`event-${eventItem.color || 'primary'} mb-4 event-list-item-container`}>
<div className="event-item-flex">
<div className="sx__month-agenda-event__title">{currentTab === 'medicalCalendar' ? eventItem.customer : eventItem.title}</div>
<div className="sx__event-modal__time">{`${moment(eventItem?.start_time).format('hh:mm A')} ${eventItem?.end_time ? `- ${moment(eventItem?.end_time).format('hh:mm A')}` : '' }`}</div>
</div>
<div className="sx__event-modal__time with-padding">{ currentTab === 'medicalCalendar' ? `provider: ${eventItem?.doctor}` : eventItem?.description}</div>
{eventItem?.event_prediction_date && <div className="sx__event-modal__time with-padding">{ `Vehicle: ${eventItem?.target_name}`}</div>}
{eventItem?.event_prediction_date && <div className="sx__event-modal__time with-padding">{ `Deadline: ${moment(eventItem?.event_prediction_date).format('MM/DD/YYYY')}`}</div>}
groupedEvents.get(key).map(eventItem => <div
key={eventItem.id}
className={`event-${eventItem.color || 'primary'} mb-3 event-list-item-container`}
onClick={() => currentTab === 'medicalCalendar' && goToView(eventItem.id)}
style={{ cursor: currentTab === 'medicalCalendar' ? 'pointer' : 'default', padding: '8px 12px', borderRadius: '4px' }}
>
{/* Medical Events */}
{currentTab === 'medicalCalendar' && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span className="sx__month-agenda-event__title">{formatFullName(eventItem.customer)}</span>
<span style={{ fontSize: '12px' }}>{moment(eventItem?.start_time).format('HH:mm')} - {moment(eventItem?.stop_time || eventItem?.start_time).format('HH:mm')}</span>
</div>
<div className="sx__event-modal__time" style={{ fontSize: '12px', marginTop: '4px' }}>Provider: {eventItem?.doctor || '-'}</div>
</>
)}
{/* Activities */}
{currentTab === 'activitiesCalendar' && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span className="sx__month-agenda-event__title">{eventItem.title}</span>
<span style={{ fontSize: '12px' }}>{moment(eventItem?.start_time).format('HH:mm')}</span>
</div>
<div className="sx__event-modal__time" style={{ fontSize: '12px', marginTop: '4px' }}>Location: {eventItem?.event_location || '-'}</div>
</>
)}
{/* Attendance Notes */}
{currentTab === 'incidentsCalendar' && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span className="sx__month-agenda-event__title">{eventItem?.target_name || eventItem.title}</span>
<span style={{ fontSize: '12px' }}>{moment(eventItem?.start_time).format('MM/DD')}</span>
</div>
<div className="sx__event-modal__time" style={{ fontSize: '12px', marginTop: '4px' }}>Reason: {eventItem?.description || '-'}</div>
</>
)}
{/* Meal Plan */}
{currentTab === 'mealPlanCalendar' && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span className="sx__month-agenda-event__title">{eventItem.title}</span>
<span style={{ fontSize: '12px', textTransform: 'capitalize' }}>{eventItem?.meal_type || '-'}</span>
</div>
<div className="sx__event-modal__time" style={{ fontSize: '12px', marginTop: '4px' }}>Ingredients: {eventItem?.ingredients || '-'}</div>
</>
)}
{/* Important Dates / Reminders */}
{currentTab === 'reminderDatesCalendar' && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span className="sx__month-agenda-event__title">{eventItem.title}</span>
<span style={{ fontSize: '12px' }}>{moment(eventItem?.start_time).format('MM/DD')}</span>
</div>
<div className="sx__event-modal__time" style={{ fontSize: '12px', marginTop: '4px' }}>{eventItem?.target_type === 'vehicle' ? 'Vehicle' : 'Person'}: {eventItem?.target_name || '-'}</div>
{eventItem?.event_prediction_date && <div className="sx__event-modal__time" style={{ fontSize: '12px', marginTop: '2px' }}>Deadline: {moment(eventItem?.event_prediction_date).format('MM/DD/YYYY')}</div>}
</>
)}
</div>)
}
</>
</div>
})
}
</div>
@ -383,54 +579,162 @@ const handleClose = () => {
setNewEventTargetType('');
setShowCreationModal(false);
setNewEventEndDateTime(undefined);
setNewEventColor('');
setNewEventRecurring(undefined);
setNewEventReminderType('');
// Reset medical fields
setNewEventCustomer('');
setNewEventResource('');
setNewEventInterpreter('');
setNewEventFasting('');
setNewEventNeedId('');
setNewEventNewPatient('');
setNewEventDisability('');
setNewEventTransMethod('');
// Reset activity fields
setNewActivityCategory('');
// Reset attendance note fields
setNewAttendanceCustomer(null);
setNewAttendanceReason('');
// Reset meal plan fields
setNewMealType('');
setNewMealIngredients('');
// Reset important dates fields
setNewReminderTitleCategory('');
setNewReminderAssociatedEntity(null);
}
const handleSave = () => {
const data = {
title: newEventTitle,
description: newEventDescription,
const userName = localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name;
let data = {
type: newEventType,
department: newEventDepartment,
start_time: newEventStartDateTime,
stop_time: newEventStartDateTime,
color: newEventColor,
source_type: newEventSourceType,
source_uuid: newEventSource?.value,
source_name: newEventSource?.label,
target_type: newEventTargetType,
target_uuid: newEventTarget?.value,
target_name: newEventTarget?.label,
event_location: newEventLocation,
event_prediction_date: newEventFutureDate && moment(newEventFutureDate).format('MM/DD/YYYY'),
event_reminder_type: newEventReminderType,
rrule: newEventRecurring,
status: 'active',
create_by: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name,
edit_by: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name,
create_by: userName,
edit_by: userName,
edit_date: new Date(),
create_date: new Date(),
edit_history: [{ employee: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name, date: new Date() }]
edit_history: [{ employee: userName, date: new Date() }]
};
// Handle Medical Appointments
if (currentTab === 'medicalCalendar') {
const selectedCustomer = customers.find(c => c.id === newEventCustomer);
const selectedResource = resources.find(r => r.id === newEventResource);
data = {
...data,
title: selectedCustomer ? `${selectedCustomer.name} - Medical Appointment` : 'Medical Appointment',
start_time: newEventStartDateTime,
stop_time: newEventStartDateTime,
color: newEventColor,
confirmed: true,
data: {
customer: newEventCustomer,
client_name: selectedCustomer?.name || '',
resource: newEventResource,
resource_name: selectedResource?.name || '',
resource_phone: selectedResource?.phone || '',
resource_address: selectedResource?.address || '',
interpreter: newEventInterpreter,
fasting: newEventFasting,
need_id: newEventNeedId,
new_patient: newEventNewPatient,
disability: newEventDisability,
trans_method: newEventTransMethod,
}
};
}
// Handle Activities
if (currentTab === 'activitiesCalendar') {
data = {
...data,
title: newEventTitle,
start_time: newEventStartDateTime,
stop_time: newEventStartDateTime,
activity_category: newActivityCategory,
color: newActivityCategory, // category maps to color for display
event_location: newEventLocation,
rrule: newEventRecurring,
};
}
// Handle Attendance Notes (Incidents)
if (currentTab === 'incidentsCalendar') {
data = {
...data,
title: newAttendanceCustomer?.label ? `Attendance Note - ${newAttendanceCustomer.label}` : 'Attendance Note',
start_time: newEventStartDateTime,
stop_time: newEventStartDateTime,
target_type: 'customer',
target_uuid: newAttendanceCustomer?.value,
target_name: newAttendanceCustomer?.label,
description: newAttendanceReason,
color: 'blue', // Default color for attendance notes
};
}
// Handle Meal Plan
if (currentTab === 'mealPlanCalendar') {
data = {
...data,
title: newEventTitle,
start_time: newEventStartDateTime,
stop_time: newEventStartDateTime,
meal_type: newMealType,
color: newMealType === 'breakfast' ? 'brown' : (newMealType === 'lunch' ? 'green' : 'red'),
ingredients: newMealIngredients,
rrule: newEventRecurring,
};
}
// Handle Important Dates (Reminders)
if (currentTab === 'reminderDatesCalendar') {
const isMemberCategory = ['birthday', 'adcaps_completion', 'center_qualification_expiration'].includes(newReminderTitleCategory);
data = {
...data,
title: getReminderTitleLabel(newReminderTitleCategory),
start_time: newEventStartDateTime,
stop_time: newEventStartDateTime,
event_reminder_type: newReminderTitleCategory,
target_type: isMemberCategory ? 'customer' : 'vehicle',
target_uuid: newReminderAssociatedEntity?.value,
target_name: newReminderAssociatedEntity?.label,
rrule: newEventRecurring,
color: isMemberCategory ? 'blue' : 'orange',
};
}
EventsService.createNewEvent(data).then(() => {
EventsService.getAllEvents({ from: EventsService.formatDate(fromDate), to: EventsService.formatDate(toDate) }).then((data) => {
setAllEvents(data.data);
setShowCreationModal(false);
setNewEventDescription('');
setNewEventTitle('');
setNewEventLocation('');
setNewEventSource(undefined);
setNewEventTarget(undefined);
setNewEventStartDateTime(undefined);
setNewEventType('');
handleClose();
})
});
}
// Helper function to get reminder title label
const getReminderTitleLabel = (value) => {
const memberOptions = {
'birthday': 'Birthday',
'adcaps_completion': 'ADCAPS Completion',
'center_qualification_expiration': 'Center Qualification Expiration'
};
const vehicleOptions = {
'oil_change': 'Oil Change',
'monthly_vehicle_inspection': 'Monthly Vehicle Inspection',
'emissions_inspection': 'Emissions Inspection',
'insurance_expiration': 'Insurance Expiration',
'license_plate_expiration': 'License Plate Expiration'
};
return memberOptions[value] || vehicleOptions[value] || value;
};
return (
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>General</Breadcrumb.Item>
<Breadcrumb.Item href="/">General</Breadcrumb.Item>
<Breadcrumb.Item active>
Calendar
</Breadcrumb.Item>
@ -451,7 +755,7 @@ const handleSave = () => {
<Tab eventKey="activitiesCalendar" title="Activities">
</Tab>
<Tab eventKey="incidentsCalendar" title="Important Notes And Incidents">
<Tab eventKey="incidentsCalendar" title="Attendance">
</Tab>
<Tab eventKey="mealPlanCalendar" title="Meal Plan">
@ -476,189 +780,283 @@ const handleSave = () => {
<Dropdown.Menu as={customMenu}/>
</Dropdown>
</div>
<Modal show={showCreationModal && currentTab !== 'medicalCalendar'} onHide={handleClose}>
<Modal show={showCreationModal} onHide={handleClose} size="sm" dialogClassName="calendar-event-modal">
<Modal.Header closeButton>
<Modal.Title>Create New Calendar Item</Modal.Title>
<Modal.Title>
{currentTab === 'medicalCalendar' && 'New Medical Appointment'}
{currentTab === 'activitiesCalendar' && 'New Activity'}
{currentTab === 'incidentsCalendar' && 'New Attendance Note'}
{currentTab === 'mealPlanCalendar' && 'New Meal Item'}
{currentTab === 'reminderDatesCalendar' && 'New Important Date'}
</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Title
<span className="required">*</span>
</div>
<input type="text" placeholder="Briefly Describe activity, incident, menu or reminder" value={newEventTitle || ''} onChange={e => setNewEventTitle(e.target.value)}/>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Description
<span className="required">*</span>
</div>
<textarea type="text" placeholder="Details for activity, incident, menu item and ingredients or reminder," value={newEventDescription || ''} onChange={e => setNewEventDescription(e.target.value)}/>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Type
<span className="required">*</span>
</div>
<div className="field-value">{newEventType}</div>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Want event to be recurring?
<select value={newEventRecurring} onChange={(e) => setNewEventRecurring(e.target.value)}>
<option value=""></option>
<option value="FREQ=YEARLY">Yearly</option>
<option value="FREQ=MONTHLY">Monthly</option>
<option value="FREQ=WEEKLY">Weekly</option>
<option value="FREQ=DAILY">Daily</option>
{/* Medical Appointment Fields */}
{currentTab === 'medicalCalendar' && (
<>
<div className="mb-3">
<div className="field-label">Customer<span className="required">*</span></div>
<select className="form-control" value={newEventCustomer} onChange={(e) => setNewEventCustomer(e.target.value)}>
<option value="">Select Customer</option>
{customers.filter(item => item?.status === 'active' && item?.type !== 'discharged').map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</div>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Start Time
<span className="required">*</span>
<div className="mb-3">
<div className="field-label">Provider<span className="required">*</span></div>
<select className="form-control" value={newEventResource} onChange={(e) => setNewEventResource(e.target.value)}>
<option value="">Select Provider</option>
{resources.filter(item => item?.status === 'active').map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</div>
<DatePicker
selected={newEventStartDateTime}
onChange={setNewEventStartDateTime}
showTimeInput
timeInputLabel="Time:"
dateFormat="MM/dd/yyyy, HH:mm"
></DatePicker>
</div>
<div className="me-4">
<div className="field-label">End Time
<span className="required">*</span>
<span className="field-blurb float-right">Make sure end time is later than start time </span>
<div className="mb-3">
<div className="field-label">Appointment Time<span className="required">*</span></div>
<DatePicker
className="form-control"
selected={newEventStartDateTime}
onChange={setNewEventStartDateTime}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="MM/dd/yyyy, HH:mm"
/>
</div>
<DatePicker
selected={newEventEndDateTime}
onChange={setNewEventEndDateTime}
showTimeInput
timeInputLabel="Time:"
dateFormat="MM/dd/yyyy, HH:mm"
></DatePicker>
</div>
</div>
{ currentTab !== 'mealPlanCalendar' && <> <div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Who will we take care of in this event:</div>
<select value={newEventTargetType} onChange={(e) => setNewEventTargetType(e.target.value)}>
<option value=""></option>
<option value="vehicle">Vehicle</option>
<option value="customer">Customer</option>
<option value="employee">Employee</option>
<option value="notApplicable">Not Applicable</option>
</select>
</div>
{newEventTargetType && newEventTargetType !== '' && newEventTargetType !== 'notApplicable' && <div className="me-4">
<div className="field-label">Please select a candidate</div>
<select value={newEventTarget?.value || ''} onChange={(e) => {
const selectedOption = e.target.options[e.target.selectedIndex];
setNewEventTarget({
value: selectedOption.value,
label: selectedOption.text
})
}}>
<option value=""></option>
{
newEventTargetType === 'vehicle' && vehicles.map((item) => <option value={item?.id}>{item?.vehicle_number}</option>)
}
{
newEventTargetType === 'customer' && customers.map((item) => <option value={item?.id}>{item?.name}</option>)
}
{
newEventTargetType === 'employee' && employees.map((item) => <option value={item?.id}>{item?.name}</option>)
}
</select>
</div>}
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Who will be responsible for this event</div>
<select value={newEventSourceType} onChange={(e) => setNewEventSourceType(e.target.value)}>
<option value=""></option>
<option value="employee">Employee</option>
<option value="resource">Resource</option>
<option value="notApplicable">Not Applicable</option>
</select>
</div>
{newEventSourceType && newEventSourceType !== '' && newEventSourceType !== 'notApplicable' && <div className="me-4">
<div className="field-label">Please select a member</div>
<select value={newEventSource?.value || ''} onChange={(e) => {
const selectedOption = e.target.options[e.target.selectedIndex];
setNewEventSource({
value: selectedOption.value,
label: selectedOption.text
})
}}>
<option value=""></option>
{
newEventSourceType === 'resource' && resources.map((item) => <option value={item?.id}>{item?.name}</option>)
}
{
newEventSourceType === 'employee' && employees.map((item) => <option value={item?.id}>{item?.name}</option>)
}
</select>
</div>}
</div> </>}
<div className="app-main-content-fields-section">
{currentTab === 'reminderDatesCalendar' && <div className="me-4">
<div className="field-label">Reminder Type
<div className="mb-3">
<div className="field-label">Language Support</div>
<select className="form-control" value={newEventInterpreter} onChange={(e) => setNewEventInterpreter(e.target.value)}>
<option value="">Select</option>
<option value="Checkin">Checkin</option>
<option value="Center">Center</option>
<option value="Family">Family</option>
<option value="Office in person">Office in person</option>
<option value="Office by phone">Office by phone</option>
<option value="Nurse">Nurse</option>
</select>
</div>
<select value={newEventReminderType} onChange={(e) => setNewEventReminderType(e.target.value)}>
<option value=""></option>
<option value="birthday">Birthday</option>
<option value="membership">Membership Renew</option>
<option value="payment">Customer Payment Due Date</option>
<option value="insurance_renew">Customer Insurance Renew</option>
<option value="insurance_expire">Vehicle Insurance Renew</option>
<option value="title_expire">Vehicle Title Registration</option>
<option value="emission_test">Vehicle Emission Test</option>
<option value="oil_change">Vehicle Oil Change</option>
</select>
</div>}
{currentTab === 'activitiesCalendar' && <div className="me-4">
<div className="field-label">Event Location
<div className="mb-3">
<div className="field-label">Transportation Support</div>
<select className="form-control" value={newEventTransMethod} onChange={(e) => setNewEventTransMethod(e.target.value)}>
<option value="">Select</option>
<option value="ByOwn">Self-Transport</option>
<option value="By Center Transportation">By Center Transportation</option>
<option value="DropOff Only">Drop-Off Only</option>
<option value="Pickup Only">Pick-Up Only</option>
<option value="Client Does Not need to Go">Medication Pickup Only</option>
</select>
</div>
<input type="text" placeholder="Type in the location this event gonna happen if applicable" value={newEventLocation || ''} onChange={e => setNewEventLocation(e.target.value)}/>
</div>}
{currentTab === 'reminderDatesCalendar' && <div className="me-4">
<div className="field-label">If this is reminder which will happen later, please select the accurate Date it gonna happen:
<div className="mb-3">
<div className="field-label">Label</div>
<select className="form-control" value={newEventColor} onChange={(e) => setNewEventColor(e.target.value)}>
<option value="">Select</option>
{EventsService.labelOptions?.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
</select>
</div>
<DatePicker selected={newEventFutureDate}
onChange={setNewEventFutureDate} dateFormat="MM/dd/yyyy"/>
</div>}
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Color
</>
)}
{/* Activity Fields */}
{currentTab === 'activitiesCalendar' && (
<>
<div className="mb-3">
<div className="field-label">Activity Name<span className="required">*</span></div>
<input type="text" className="form-control" placeholder="Enter activity name" value={newEventTitle || ''} onChange={e => setNewEventTitle(e.target.value)}/>
</div>
<select value={newEventColor} onChange={e => setNewEventColor(e.target.value)}>
<option value=""></option>
{
EventsService.colorOptions?.map((item) => <option value={item?.value}>{item?.label}</option>)
}
</select>
</div>
</div>
<div className="mb-3">
<div className="field-label">Date & Time</div>
<DatePicker
className="form-control"
selected={newEventStartDateTime}
onChange={setNewEventStartDateTime}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="MM/dd/yyyy, HH:mm"
/>
</div>
<div className="mb-3">
<div className="field-label">Category<span className="required">*</span></div>
<select className="form-control" value={newActivityCategory} onChange={(e) => setNewActivityCategory(e.target.value)}>
<option value="">Select Category</option>
<option value="red">Classes</option>
<option value="pink">Games</option>
<option value="green">Events</option>
<option value="blue">Outings</option>
<option value="purple">Personal Care</option>
<option value="brown">Care Activities</option>
</select>
</div>
<div className="mb-3">
<div className="field-label">Location</div>
<input type="text" className="form-control" placeholder="Enter location" value={newEventLocation || ''} onChange={e => setNewEventLocation(e.target.value)}/>
</div>
<div className="mb-3">
<div className="field-label">Repeat</div>
<select className="form-control" value={newEventRecurring} onChange={(e) => setNewEventRecurring(e.target.value)}>
<option value="">No (One-time)</option>
<option value="FREQ=DAILY">Daily</option>
<option value="FREQ=WEEKLY">Weekly</option>
<option value="FREQ=MONTHLY">Monthly</option>
<option value="FREQ=YEARLY">Yearly</option>
</select>
</div>
</>
)}
{/* Attendance Note Fields */}
{currentTab === 'incidentsCalendar' && (
<>
<div className="mb-3">
<div className="field-label">Customer Name<span className="required">*</span></div>
<Select
value={newAttendanceCustomer}
onChange={setNewAttendanceCustomer}
options={customers.filter(item => item?.status === 'active' && item?.type !== 'discharged').map(c => ({ value: c.id, label: c.name }))}
placeholder="Select Customer"
isClearable
/>
</div>
<div className="mb-3">
<div className="field-label">Date<span className="required">*</span></div>
<DatePicker
className="form-control"
selected={newEventStartDateTime}
onChange={setNewEventStartDateTime}
dateFormat="MM/dd/yyyy"
/>
</div>
<div className="mb-3">
<div className="field-label">Reason</div>
<input type="text" className="form-control" placeholder="Enter reason" value={newAttendanceReason || ''} onChange={e => setNewAttendanceReason(e.target.value)}/>
</div>
</>
)}
{/* Meal Item Fields */}
{currentTab === 'mealPlanCalendar' && (
<>
<div className="mb-3">
<div className="field-label">Dish Name<span className="required">*</span></div>
<input type="text" className="form-control" placeholder="Enter dish name" value={newEventTitle || ''} onChange={e => setNewEventTitle(e.target.value)}/>
</div>
<div className="mb-3">
<div className="field-label">Meal Type<span className="required">*</span></div>
<select className="form-control" value={newMealType} onChange={(e) => setNewMealType(e.target.value)}>
<option value="">Select Meal Type</option>
<option value="breakfast">Breakfast</option>
<option value="lunch">Lunch</option>
<option value="snack">Snack</option>
</select>
</div>
<div className="mb-3">
<div className="field-label">Date</div>
<DatePicker
className="form-control"
selected={newEventStartDateTime}
onChange={setNewEventStartDateTime}
dateFormat="MM/dd/yyyy"
/>
</div>
<div className="mb-3">
<div className="field-label">Repeat</div>
<select className="form-control" value={newEventRecurring} onChange={(e) => setNewEventRecurring(e.target.value)}>
<option value="">No (One-time)</option>
<option value="FREQ=DAILY">Daily</option>
<option value="FREQ=WEEKLY">Weekly</option>
<option value="FREQ=MONTHLY">Monthly</option>
</select>
</div>
<div className="mb-3">
<div className="field-label">Ingredients</div>
<input type="text" className="form-control" placeholder="Enter ingredients" value={newMealIngredients || ''} onChange={e => setNewMealIngredients(e.target.value)}/>
</div>
</>
)}
{/* Important Dates Fields */}
{currentTab === 'reminderDatesCalendar' && (
<>
<div className="mb-3">
<div className="field-label">Title<span className="required">*</span></div>
<select className="form-control" value={newReminderTitleCategory} onChange={(e) => {
setNewReminderTitleCategory(e.target.value);
setNewReminderAssociatedEntity(null); // Reset when category changes
}}>
<option value="">Select Title</option>
<optgroup label="Member">
<option value="birthday">Birthday</option>
<option value="adcaps_completion">ADCAPS Completion</option>
<option value="center_qualification_expiration">Center Qualification Expiration</option>
</optgroup>
<optgroup label="Vehicle">
<option value="oil_change">Oil Change</option>
<option value="monthly_vehicle_inspection">Monthly Vehicle Inspection</option>
<option value="emissions_inspection">Emissions Inspection</option>
<option value="insurance_expiration">Insurance Expiration</option>
<option value="license_plate_expiration">License Plate Expiration</option>
</optgroup>
</select>
</div>
{newReminderTitleCategory && (
<div className="mb-3">
<div className="field-label">
{['birthday', 'adcaps_completion', 'center_qualification_expiration'].includes(newReminderTitleCategory)
? 'Associated Person'
: 'Associated Vehicle'}<span className="required">*</span>
</div>
<Select
value={newReminderAssociatedEntity}
onChange={setNewReminderAssociatedEntity}
options={
['birthday', 'adcaps_completion', 'center_qualification_expiration'].includes(newReminderTitleCategory)
? customers.filter(item => item?.status === 'active' && item?.type !== 'discharged').map(c => ({ value: c.id, label: c.name }))
: vehicles.map(v => ({ value: v.id, label: v.vehicle_number }))
}
placeholder={['birthday', 'adcaps_completion', 'center_qualification_expiration'].includes(newReminderTitleCategory) ? 'Select Person' : 'Select Vehicle'}
isClearable
/>
</div>
)}
<div className="mb-3">
<div className="field-label">Date</div>
<DatePicker
className="form-control"
selected={newEventStartDateTime}
onChange={setNewEventStartDateTime}
dateFormat="MM/dd/yyyy"
/>
</div>
<div className="mb-3">
<div className="field-label">Category</div>
<div className="field-value" style={{ padding: '8px 0', color: '#666' }}>
{newReminderTitleCategory
? (['birthday', 'adcaps_completion', 'center_qualification_expiration'].includes(newReminderTitleCategory) ? 'Member Related' : 'Vehicle Related')
: '-'}
</div>
</div>
<div className="mb-3">
<div className="field-label">Repeat Cycle</div>
<select className="form-control" value={newEventRecurring} onChange={(e) => setNewEventRecurring(e.target.value)}>
<option value="">No (One-time)</option>
<option value="FREQ=DAILY">Daily</option>
<option value="FREQ=WEEKLY">Weekly</option>
<option value="FREQ=MONTHLY">Monthly</option>
<option value="FREQ=YEARLY">Yearly</option>
</select>
</div>
</>
)}
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleClose}>
Close
<Button variant="secondary" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button variant="primary" onClick={handleSave}>
Save Calendar Item
<Button variant="primary" size="sm" onClick={handleSave} disabled={
(currentTab === 'medicalCalendar' && (!newEventCustomer || !newEventResource || !newEventStartDateTime)) ||
(currentTab === 'activitiesCalendar' && (!newEventTitle || !newActivityCategory)) ||
(currentTab === 'incidentsCalendar' && (!newAttendanceCustomer || !newEventStartDateTime)) ||
(currentTab === 'mealPlanCalendar' && (!newEventTitle || !newMealType)) ||
(currentTab === 'reminderDatesCalendar' && (!newReminderTitleCategory || !newReminderAssociatedEntity))
}>
Save
</Button>
</Modal.Footer>
</Modal>

View File

@ -49,7 +49,7 @@ const CenterPhoneList = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>General</Breadcrumb.Item>
<Breadcrumb.Item href="/">General</Breadcrumb.Item>
<Breadcrumb.Item active>
Center Phone
</Breadcrumb.Item>

View File

@ -28,7 +28,29 @@ const CreateCenterPhone = () => {
}
const validatePhone = () => {
const errors = [];
// Required fields validation
if (!phoneTitle || phoneTitle.trim() === '') {
errors.push('Phone Title');
}
if (!phoneNumber || phoneNumber.trim() === '') {
errors.push('Phone Number');
}
if (errors.length > 0) {
window.alert(`Please fill in the following required fields:\n${errors.join('\n')}`);
return false;
}
return true;
};
const savePhone = () => {
if (!validatePhone()) {
return;
}
const data = {
phone_title: phoneTitle,
phone_number: phoneNumber
@ -43,10 +65,13 @@ const CreateCenterPhone = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>General</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/">General</Breadcrumb.Item>
<Breadcrumb.Item href="/center-phones/list">
Center Phone
</Breadcrumb.Item>
<Breadcrumb.Item active>
Create Center Phone
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>

View File

@ -35,7 +35,29 @@ const UpdateCenterPhone = () => {
navigate('/center-phones/list')
}
const validatePhone = () => {
const errors = [];
// Required fields validation
if (!phoneTitle || phoneTitle.trim() === '') {
errors.push('Phone Title');
}
if (!phoneNumber || phoneNumber.trim() === '') {
errors.push('Phone Number');
}
if (errors.length > 0) {
window.alert(`Please fill in the following required fields:\n${errors.join('\n')}`);
return false;
}
return true;
};
const savePhone = () => {
if (!validatePhone()) {
return;
}
const data = {
phone_title: phoneTitle,
phone_number: phoneNumber,
@ -48,10 +70,13 @@ const UpdateCenterPhone = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>General</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/">General</Breadcrumb.Item>
<Breadcrumb.Item href="/center-phones/list">
Center Phone
</Breadcrumb.Item>
<Breadcrumb.Item active>
Update Center Phone
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import { useNavigate, useParams } from "react-router-dom";
import { customerSlice } from "./../../store";
import { AuthService, CustomerService, EventsService, LabelService } from "../../services";
import { CUSTOMER_TYPE, ManageTable, Export } from "../../shared";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab, Dropdown } from "react-bootstrap";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab, Dropdown, OverlayTrigger, Popover } from "react-bootstrap";
import { Columns, Download, Filter, PencilSquare, PersonSquare, Plus } from "react-bootstrap-icons";
@ -26,6 +26,12 @@ const CustomersList = () => {
const [serviceRequirementFilter, setServiceRequirementFilter] = useState('');
const [tagsFilter, setTagsFilter] = useState([]);
const [availableLabels, setAvailableLabels] = useState([]);
const [customerAvatars, setCustomerAvatars] = useState({});
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(25);
const [loadingAvatarId, setLoadingAvatarId] = useState(null);
const [columns, setColumns] = useState([
{
key: 'name',
@ -126,22 +132,42 @@ const CustomersList = () => {
navigate(`/login`);
}
CustomerService.getAllCustomers().then((data) => {
setCustomers(data.data.map((item) =>{
const customerData = data.data.map((item) =>{
item.phone = item?.phone || item?.home_phone || item?.mobile_phone;
item.address = item?.address1 || item?.address2 || item?.address3 || item?.address4|| item?.address5;
return item;
}).sort((a, b) => a.lastname > b.lastname ? 1: -1));
}).sort((a, b) => a.lastname > b.lastname ? 1: -1);
setCustomers(customerData);
// Removed bulk avatar loading - now loaded on-demand when user clicks
})
LabelService.getAll().then((data) => {
setAvailableLabels(data.data);
})
// EventsService.getAllEvents({ from: EventsService.formatDate(new Date()), to: '9999-12-31'}).then((data) => {
// console.log('events', data.data)
// setEvents(data.data);
// })
}, []);
// Load avatar on-demand when user clicks on profile icon
const loadAvatarOnDemand = (customerId) => {
if (customerAvatars[customerId] !== undefined) return; // Already loaded or attempted
setLoadingAvatarId(customerId);
CustomerService.getAvatarAsBlob(`customer_avatar_${customerId}`)
.then(result => {
if (result?.data && result.data.size > 0) {
const url = URL.createObjectURL(result.data);
setCustomerAvatars(prev => ({ ...prev, [customerId]: url }));
} else {
setCustomerAvatars(prev => ({ ...prev, [customerId]: null }));
}
setLoadingAvatarId(null);
})
.catch(() => {
setCustomerAvatars(prev => ({ ...prev, [customerId]: null }));
setLoadingAvatarId(null);
});
};
useEffect(() => {
let filtered = customers;
@ -181,8 +207,47 @@ const CustomersList = () => {
}
setFilteredCustomers(filtered);
setCurrentPage(1); // Reset to first page when filters change
}, [keyword, customers, showInactive, healthConditionFilter, paymentStatusFilter, serviceRequirementFilter, tagsFilter])
// Pagination calculations
const totalPages = Math.ceil(filteredCustomers.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedCustomers = filteredCustomers.slice(startIndex, endIndex);
const goToPage = (page) => {
if (page >= 1 && page <= totalPages) {
setCurrentPage(page);
}
};
const getPageNumbers = () => {
const pages = [];
const maxVisiblePages = 5;
if (totalPages <= maxVisiblePages) {
for (let i = 1; i <= totalPages; i++) pages.push(i);
} else {
if (currentPage <= 3) {
for (let i = 1; i <= 4; i++) pages.push(i);
pages.push('...');
pages.push(totalPages);
} else if (currentPage >= totalPages - 2) {
pages.push(1);
pages.push('...');
for (let i = totalPages - 3; i <= totalPages; i++) pages.push(i);
} else {
pages.push(1);
pages.push('...');
for (let i = currentPage - 1; i <= currentPage + 1; i++) pages.push(i);
pages.push('...');
pages.push(totalPages);
}
}
return pages;
};
useEffect(() => {
const newCustomers = [...customers];
const sortedCustomers = sorting.key === '' ? newCustomers : newCustomers.sort((a, b) => {
@ -377,13 +442,31 @@ const CustomersList = () => {
const table = <div className="list row mb-4">
<div className="col-md-12">
<table className="personnel-info-table">
<div style={{
maxHeight: '600px',
overflowY: 'auto',
overflowX: 'auto',
position: 'relative'
}}>
<table className="personnel-info-table" style={{ position: 'relative' }}>
<thead>
<tr>
<th className="th-checkbox"><input type="checkbox" checked={checkSelectAll()} onClick={() => toggleSelectedAllItems()}></input></th>
<th className="th-index">No.</th>
<th className="th-checkbox" style={{ position: 'sticky', left: 0, zIndex: 3, backgroundColor: '#0066B1', paddingLeft: '14px' }}><input type="checkbox" checked={checkSelectAll()} onClick={() => toggleSelectedAllItems()}></input></th>
<th className="th-index" style={{ position: 'sticky', left: '50px', zIndex: 3, backgroundColor: '#0066B1', paddingLeft: '14px' }}>No.</th>
{
columns.filter(col => col.show).map((column, index) => <th className="sortable-header" key={index}>
columns.filter(col => col.show).map((column, index) => <th
className="sortable-header"
key={index}
style={column.key === 'name' ? {
position: 'sticky',
left: '100px',
zIndex: 3,
backgroundColor: '#0066B1',
paddingLeft: '14px',
//borderRight: '2px solid #0056a0',
boxShadow: '2px 0 5px rgba(0,0,0,0.1)'
} : {}}
>
{column.label} <span className="float-right" onClick={() => sortTableWithField(column.key)}><img src={`/images/${getSortingImg(column.key)}.png`}></img></span>
</th>)
}
@ -394,10 +477,71 @@ const CustomersList = () => {
</thead>
<tbody>
{
filteredCustomers.map((customer, index) => <tr key={customer.id}>
<td className="td-checkbox"><input type="checkbox" checked={selectedItems.includes(customer.id)} onClick={()=>toggleItem(customer?.id)}/></td>
<td className="td-index">{index + 1}</td>
{columns.find(col => col.key === 'name')?.show && <td> {AuthService.canAddOrEditCustomers() && <PencilSquare size={16} className="clickable me-2" onClick={() => goToEdit(customer?.id)}></PencilSquare>} {AuthService.canViewCustomers() && <PersonSquare onClick={() => goToView(customer?.id)} size={16} className="clickable me-2" />} {customer?.name}</td>}
paginatedCustomers.map((customer, index) => {
const avatarData = customerAvatars[customer.id];
const isLoadingAvatar = loadingAvatarId === customer.id;
const profilePopover = (
<Popover id={`popover-${customer.id}`}>
<Popover.Body style={{ textAlign: 'center' }}>
{isLoadingAvatar ? (
<Spinner animation="border" size="sm" />
) : avatarData ? (
<img
src={avatarData}
alt={customer.name}
style={{ width: '200px', height: '200px', objectFit: 'cover', borderRadius: '8px' }}
/>
) : (
<PersonSquare size={200} />
)}
</Popover.Body>
</Popover>
);
const actualIndex = startIndex + index; // Calculate actual index for display
const rowBgColor = index % 2 === 0 ? 'white' : '#eee';
const stickyBaseStyle = {
position: 'sticky',
zIndex: 2,
paddingLeft: '14px',
};
const checkboxStickyStyle = {
...stickyBaseStyle,
left: 0,
backgroundColor: rowBgColor,
};
const indexStickyStyle = {
...stickyBaseStyle,
left: '50px',
backgroundColor: rowBgColor,
};
const nameStickyStyle = {
...stickyBaseStyle,
left: '100px',
backgroundColor: rowBgColor,
boxShadow: '2px 0 5px rgba(0,0,0,0.1)'
};
return <tr key={customer.id}>
<td className="td-checkbox" style={checkboxStickyStyle}><input type="checkbox" checked={selectedItems.includes(customer.id)} onClick={()=>toggleItem(customer?.id)}/></td>
<td className="td-index" style={indexStickyStyle}>{actualIndex + 1}</td>
{columns.find(col => col.key === 'name')?.show && <td style={nameStickyStyle}>
{AuthService.canAddOrEditCustomers() && <PencilSquare size={16} className="clickable me-2" onClick={() => goToEdit(customer?.id)}></PencilSquare>}
{AuthService.canViewCustomers() && (
<OverlayTrigger
trigger="click"
placement="right"
overlay={profilePopover}
rootClose
>
<span style={{ cursor: 'pointer' }} onClick={() => loadAvatarOnDemand(customer.id)}>
<PersonSquare size={16} className="clickable me-2" />
</span>
</OverlayTrigger>
)}
<span className="clickable" onClick={() => goToView(customer?.id)} style={{ textDecoration: 'underline', cursor: 'pointer' }}>{customer?.name}</span>
</td>}
{columns.find(col => col.key === 'chinese_name')?.show && <td>{customer?.name_cn}</td>}
{columns.find(col => col.key === 'email')?.show && <td>{customer?.email}</td>}
{columns.find(col => col.key === 'type')?.show && <td>{customer?.type}</td>}
@ -432,10 +576,74 @@ const CustomersList = () => {
}
</td>
</tr>)
</tr>
})
}
</tbody>
</table>
</div>
{/* Pagination Controls */}
{totalPages > 1 && (
<div className="pagination-controls" style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '15px 0',
marginTop: '10px'
}}>
<div style={{ color: '#666', fontSize: '13px' }}>
Showing {startIndex + 1} - {Math.min(endIndex, filteredCustomers.length)} of {filteredCustomers.length} customers
</div>
<div style={{ display: 'flex', gap: '5px', alignItems: 'center' }}>
<button
className="btn btn-sm btn-outline-primary"
onClick={() => goToPage(1)}
disabled={currentPage === 1}
style={{ padding: '4px 8px' }}
>
First
</button>
<button
className="btn btn-sm btn-outline-primary"
onClick={() => goToPage(currentPage - 1)}
disabled={currentPage === 1}
style={{ padding: '4px 8px' }}
>
Prev
</button>
{getPageNumbers().map((page, index) => (
page === '...' ? (
<span key={`ellipsis-${index}`} style={{ padding: '4px 8px' }}>...</span>
) : (
<button
key={page}
className={`btn btn-sm ${currentPage === page ? 'btn-primary' : 'btn-outline-primary'}`}
onClick={() => goToPage(page)}
style={{ padding: '4px 10px', minWidth: '35px' }}
>
{page}
</button>
)
))}
<button
className="btn btn-sm btn-outline-primary"
onClick={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages}
style={{ padding: '4px 8px' }}
>
Next
</button>
<button
className="btn btn-sm btn-outline-primary"
onClick={() => goToPage(totalPages)}
disabled={currentPage === totalPages}
style={{ padding: '4px 8px' }}
>
Last
</button>
</div>
</div>
)}
</div>
</div>;
@ -521,7 +729,7 @@ const CustomersList = () => {
</div>}
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>General</Breadcrumb.Item>
<Breadcrumb.Item href="/">General</Breadcrumb.Item>
<Breadcrumb.Item active>
Customer Information
</Breadcrumb.Item>
@ -534,13 +742,13 @@ const CustomersList = () => {
</h4>
</div>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-container list-page">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="activeCustomers" id="customers-tab" onSelect={(k) => showArchive(k)}>
<Tab eventKey="activeCustomers" title="Active Customers">
{table}
</Tab>
<Tab eventKey="archivedCustomers" title="Archived Customers">
<Tab eventKey="archivedCustomers" title="Discharge Customers">
{table}
</Tab>
</Tabs>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -268,7 +268,7 @@ const DashboardCustomersList = () => {
}
const table = <div className="list row mb-4">
<div className="col-md-12">
<div className="col-md-12" style={{ overflow: 'auto'}}>
<table className="personnel-info-table">
<thead>
<tr>
@ -397,7 +397,7 @@ const DashboardCustomersList = () => {
<Tab eventKey="activeCustomers" title="Active Customers">
{table}
</Tab>
<Tab eventKey="archivedCustomers" title="Archived Customers">
<Tab eventKey="archivedCustomers" title="Discharge Customers">
{table}
</Tab>
</Tabs>

View File

@ -37,7 +37,29 @@ const CreateEventRequest = () => {
}
const validateEventRequest = () => {
const errors = [];
// Required fields validation
if (!currentCustomer || !currentCustomer.id) {
errors.push('Patient');
}
if (!currentResource || !currentResource.id) {
errors.push('Doctor');
}
if (errors.length > 0) {
window.alert(`Please fill in the following required fields:\n${errors.join('\n')}`);
return false;
}
return true;
};
const saveEventRequest = () => {
if (!validateEventRequest()) {
return;
}
const newEventRequest = {
customer_id: currentCustomer?.id,
customer_display: `${currentCustomer?.name} - ${currentCustomer?.name_cn} - ${currentCustomer?.birth_date}`,
@ -112,8 +134,8 @@ const CreateEventRequest = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Medical</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/medical/index">Medical</Breadcrumb.Item>
<Breadcrumb.Item href="/medical/event-request/list">
Appointment Request Information
</Breadcrumb.Item>
<Breadcrumb.Item active>
@ -135,34 +157,42 @@ const CreateEventRequest = () => {
<span className="required">*</span>
</div>
<Select styles={{
control: (baseStyles, state) => ({
control: (baseStyles) => ({
...baseStyles,
width: '350px',
height: '45px',
'padding-top': 0,
'padding-bottom': 0,
'margin-top': 0,
'margin-bottom': 0
borderRadius: '8px'
}),
indicatorSeparator: (baseStyles, state) => ({
...baseStyles,
width: 0
indicatorSeparator: () => ({
display: 'none'
}),
indicatorsContainer: (baseStyles) => ({
dropdownIndicator: (baseStyles) => ({
...baseStyles,
'margin-top': '-10px'
paddingRight: '8px',
color: '#333',
'&:hover': { color: '#000' }
}),
valueContainer: (baseStyles) => ({
...baseStyles,
height: '43px',
padding: '0 8px',
}),
input: (baseStyles) => ({
...baseStyles,
margin: '0px',
padding: '0px',
height: '30px',
width: '290px'
}),
placeholder: (baseStyles) => ({
...baseStyles,
'margin-top': '-10px',
'font-size': '13px'
fontSize: '13px'
}),
singleValue: (baseStyles, state) => ({
singleValue: (baseStyles) => ({
...baseStyles,
'margin-top': '-10px',
'font-size': '13px'
margin: '0px',
fontSize: '13px'
})
}} value={customerDisplay || ''} onChange={selectedCustomer => onCustomerChange(selectedCustomer)} options={[{value: '', label: ''}, ...customers.map(customer => ({
}} value={customerDisplay || ''} onChange={selectedCustomer => onCustomerChange(selectedCustomer)} options={[{value: '', label: ''}, ...customers.filter(customer => customer?.status === 'active' && customer?.type !== 'discharged').map(customer => ({
value: customer?.id || '',
label: `${customer?.name} - ${customer?.name_cn} - ${customer?.birth_date}` || ''
}))]}></Select>

View File

@ -305,7 +305,7 @@ const EventRequestList = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Medical</Breadcrumb.Item>
<Breadcrumb.Item href="/medical/index">Medical</Breadcrumb.Item>
<Breadcrumb.Item active>
Appointment Requests Information
</Breadcrumb.Item>
@ -318,7 +318,7 @@ const EventRequestList = () => {
</h4>
</div>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-container list-page">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="activeRequests" id="requests-tab" onSelect={(k) => showDoneItems(k)}>
<Tab eventKey="activeRequests" title="Active Requests">

View File

@ -51,7 +51,29 @@ const CreateEvent = () => {
navigate(`/medical/events/edit/${id}?from=create`)
}
const validateEvent = () => {
const errors = [];
// Required fields validation
if (!currentCustomer || !currentCustomer.id) {
errors.push('Customer');
}
if (!startTime) {
errors.push('Appointment Time');
}
if (errors.length > 0) {
window.alert(`Please fill in the following required fields:\n${errors.join('\n')}`);
return false;
}
return true;
};
const saveEvent = () => {
if (!validateEvent()) {
return;
}
console.log('customer', currentCustomer);
// console.log('resource', currentResource);
@ -137,8 +159,8 @@ const CreateEvent = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Medical</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/medical/index">Medical</Breadcrumb.Item>
<Breadcrumb.Item href="/medical/events/list">
Medical Appointment Information
</Breadcrumb.Item>
<Breadcrumb.Item active>
@ -150,54 +172,62 @@ const CreateEvent = () => {
</div>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-container form-page">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="eventClientInfo" id="event-tab">
<Tab eventKey="eventClientInfo" title="Appointment Client And Time Information">
<Tab eventKey="eventClientInfo" title="Appointment Customer And Time Information">
<div className="multi-columns-container">
<div className="column-container">
<div className="column-card">
<h6 className="text-primary">Client And Start Time</h6>
<h6 className="text-primary">Customer And Appointment Time</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Client
<div className="field-label">Customer
<span className="required">*</span>
</div>
<Select styles={{
control: (baseStyles, state) => ({
control: (baseStyles) => ({
...baseStyles,
width: '350px',
height: '45px',
'padding-top': 0,
'padding-bottom': 0,
'margin-top': 0,
'margin-bottom': 0
borderRadius: '8px'
}),
indicatorSeparator: (baseStyles, state) => ({
...baseStyles,
width: 0
indicatorSeparator: () => ({
display: 'none'
}),
indicatorsContainer: (baseStyles) => ({
dropdownIndicator: (baseStyles) => ({
...baseStyles,
'margin-top': '-10px'
paddingRight: '8px',
color: '#333',
'&:hover': { color: '#000' }
}),
valueContainer: (baseStyles) => ({
...baseStyles,
height: '43px',
padding: '0 8px',
}),
input: (baseStyles) => ({
...baseStyles,
margin: '0px',
padding: '0px',
height: '30px',
width: '290px'
}),
placeholder: (baseStyles) => ({
...baseStyles,
'margin-top': '-10px',
'font-size': '13px'
fontSize: '13px'
}),
singleValue: (baseStyles, state) => ({
singleValue: (baseStyles) => ({
...baseStyles,
'margin-top': '-10px',
'font-size': '13px'
margin: '0px',
fontSize: '13px'
})
}} value={customerName || ''} onChange={selectedCustomer => onCustomerChange(selectedCustomer)} options={[{value: '', label: ''}, ...customers.map(customer => ({
}} value={customerName || ''} onChange={selectedCustomer => onCustomerChange(selectedCustomer)} options={[{value: '', label: ''}, ...customers.filter(customer => customer?.status === 'active' && customer?.type !== 'discharged').map(customer => ({
value: customer?.id || '',
label: customer?.name || ''
}))]}></Select>
</div>
<div className="me-4">
<div className="field-label">Start Time
<div className="field-label">Appointment Time
<span className="required">*</span>
</div>
<DatePicker
@ -221,10 +251,10 @@ const CreateEvent = () => {
<div className="column-card">
{
currentCustomer && <>
<h6 className="text-primary">Client Information</h6>
<h6 className="text-primary">Customer Information</h6>
<div className="app-main-content-fields-section short">
<div className="field-body">
<div className="field-label">Client Name</div>
<div className="field-label">Customer Name</div>
<div className="field-value">{currentCustomer?.name || ''}</div>
</div>
<div className="field-body">

View File

@ -37,6 +37,42 @@ const EventsCalendar = () => {
const eventsServicePlugin = createEventsServicePlugin();
const eventModalService = createEventModalPlugin();
const [groupedEvents, setGroupedEvents] = useState(new Map());
const [currentRangeStart, setCurrentRangeStart] = useState(null);
const [currentRangeEnd, setCurrentRangeEnd] = useState(null);
// Helper function to format name from "lastname, firstname" to "firstname lastname"
const formatFullName = (name) => {
if (!name) return '';
if (name.includes(',')) {
const parts = name.split(',').map(part => part.trim());
return `${parts[1]} ${parts[0]}`; // firstname lastname
}
return name;
};
// Helper function to get shortened name
const getShortenedName = (name) => {
if (!name) return '';
const fullName = formatFullName(name);
const parts = fullName.split(' ');
if (parts.length >= 2) {
return `${parts[0]} ${parts[parts.length - 1].charAt(0)}`; // FirstName L
}
return fullName;
};
// Helper function to format event title - now uses full name, CSS handles overflow
const formatEventTitle = (customerName, startTime) => {
const fullName = formatFullName(customerName);
// Return full name - CSS will handle truncation with text-overflow: ellipsis
return fullName;
};
// Get full name for description/tooltip
const getEventDescription = (customerName, doctorName) => {
const fullName = formatFullName(customerName);
return `${fullName} - ${doctorName}`;
};
const calendar = useCalendarApp({
views: [createViewMonthGrid(), createViewDay(), createViewMonthAgenda(), createViewWeek()],
@ -52,21 +88,47 @@ const EventsCalendar = () => {
events: events,
plugins: [eventModalService, eventsServicePlugin],
callbacks: {
// onRangeUpdate(range) {
// console.log('new range', range.start);
// console.log('new range end', range.end);
// },
onSelectedDateUpdate(date) {
setFromDate(new Date(new Date(date).getFullYear(), new Date(date).getMonth(), 1));
setToDate(new Date(new Date(date).getFullYear(), new Date(date).getMonth() + 1, 0));
onRangeUpdate(range) {
console.log('new calendar range start date', range.start);
console.log('new calendar range end date', range.end);
setCurrentRangeStart(range.start);
setCurrentRangeEnd(range.end);
// Update fromDate/toDate for API fetching based on the range
const startDate = new Date(range.start);
const endDate = new Date(range.end);
setFromDate(new Date(startDate.getFullYear(), startDate.getMonth(), 1));
setToDate(new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0));
}
}
})
// Filter events based on current calendar range
const getFilteredEvents = () => {
console.log("EventsCalendar - range:", currentRangeStart, "to", currentRangeEnd, "events:", events.length);
if (!currentRangeStart || !currentRangeEnd) {
// If no range set yet, show all events for current month
const now = moment();
return events.filter(event => {
const eventDate = moment(event.start_time);
return eventDate.isSame(now, 'month');
});
}
// Filter events within the calendar's visible range
const rangeStart = moment(currentRangeStart);
const rangeEnd = moment(currentRangeEnd);
return events.filter(event => {
const eventDate = moment(event.start_time);
return eventDate.isBetween(rangeStart, rangeEnd, 'day', '[]');
});
};
const getGroupedEvents = () => {
const eventsDateMap = new Map();
console.log('events', events);
for (const eventItem of events) {
const filteredEvents = getFilteredEvents();
for (const eventItem of filteredEvents) {
const dateString = moment(eventItem.start_time).format('MMM Do, YYYY');
if (eventsDateMap.has(dateString)) {
eventsDateMap.set(dateString, [...eventsDateMap.get(dateString), eventItem]);
@ -76,7 +138,6 @@ const EventsCalendar = () => {
eventsDateMap.set(dateString, value);
}
}
console.log('eventsMap', eventsDateMap);
return eventsDateMap;
}
@ -87,7 +148,7 @@ const EventsCalendar = () => {
AuthService.logout();
navigate(`/login`);
}
CustomerService.getAllCustomers().then((data) => {
CustomerService.getAllActiveCustomers().then((data) => {
setCustomers(data.data);
});
ResourceService.getAll().then((data) => {
@ -118,7 +179,8 @@ const EventsCalendar = () => {
item.endTime = item?.start_time? `${moment(new Date(item?.end_time)).format('hh:mm A')}` : '' ;
item.fasting = item?.data?.fasting || '';
item.transportation = item?.link_event_name || '';
item.title = `${customerField}, provider: ${doctorField}`;
item.title = formatEventTitle(customerField, item?.start_time);
item.description = getEventDescription(customerField, doctorField); // Full info for tooltip
item.start = item?.start_time? `${moment(new Date(item?.start_time)).format('YYYY-MM-DD HH:mm')}` : '';
item.end = item.stop_time? `${moment(new Date(item?.stop_time)).format('YYYY-MM-DD HH:mm')}` : '';
const transportationInfo = EventsService.getTransportationInfo(data.data, item, timeData);
@ -140,16 +202,15 @@ const EventsCalendar = () => {
})}
}, [fromDate, toDate, customers, resources, timeData, showDeletedItems]);
useEffect(() => {
if (events && calendar) {
// events?.forEach((item) => {
// if (!calendar.eventsService.getAll()?.find(evItem => evItem.id === item.id)) {
// calendar.eventsService.add(item)
// } } );
console.log("EventsCalendar useEffect - events:", events.length, "range:", currentRangeStart, "to", currentRangeEnd);
calendar?.eventsService?.set(events);
setGroupedEvents(getGroupedEvents());
}
}, [events]);
}, [events, currentRangeStart, currentRangeEnd]);
@ -203,7 +264,7 @@ const EventsCalendar = () => {
item.endTime = item?.start_time? `${moment(new Date(item?.end_time)).format('hh:mm A')}` : '' ;
item.fasting = item?.data?.fasting || '';
item.transportation = item?.link_event_name || '';
item.title = `${customerField}, provider: ${doctorField}`;
item.title = formatEventTitle(customerField, item?.start_time);
item.start = item?.start_time? `${moment(new Date(item?.start_time)).format('YYYY-MM-DD HH:mm')}` : '';
item.end = item.stop_time? `${moment(new Date(item?.stop_time)).format('YYYY-MM-DD HH:mm')}` : '';
item._options = { additionalClasses: [`event-${item?.color || 'primary'}`]};
@ -280,7 +341,7 @@ const EventsCalendar = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Medical</Breadcrumb.Item>
<Breadcrumb.Item href="/medical/index">Medical</Breadcrumb.Item>
<Breadcrumb.Item active>
Medical Event Calendar
</Breadcrumb.Item>
@ -301,22 +362,26 @@ const EventsCalendar = () => {
{calendar && <ScheduleXCalendar customComponents={customComponents} calendarApp={calendar} />}
</div>
<div className="column-container">
<div className="column-card">
<div className="column-card" style={{ maxHeight: '800px', overflowY: 'auto', overflowX: 'hidden' }}>
<h6 className="text-primary me-4">List</h6>
{
Array.from(groupedEvents?.keys())?.map((key) => {
return <>
return <div key={key}>
<h6 className="text-primary me-2">{key}</h6>
{
groupedEvents.get(key).map(eventItem => <div className={`event-${eventItem.color || 'primary'} mb-4 event-list-item-container`}>
groupedEvents.get(key).map(eventItem => <div
key={eventItem.id}
className={`event-${eventItem.color || 'primary'} mb-4 event-list-item-container`}
onClick={() => goToView(eventItem.id)}
style={{ cursor: 'pointer' }}
>
<div className="event-item-flex">
<div className="sx__month-agenda-event__title">{eventItem.customer}</div>
<div className="sx__event-modal__time">{`${moment(eventItem?.start_time).format('hh:mm A')} ${eventItem?.end_time ? `- ${moment(eventItem?.end_time).format('hh:mm A')}` : '' }`}</div>
<div className="sx__month-agenda-event__title">{`${moment(eventItem?.start_time).format('hh:mm A')}: ${formatFullName(eventItem.customer)}`}</div>
</div>
<div className="sx__event-modal__time with-padding">{`provider: ${eventItem?.doctor}`}</div>
</div>)
}
</>
</div>
})
}
</div>

View File

@ -13,7 +13,9 @@ const EventsList = () => {
const [customers, setCustomers] = useState([]);
const [resources, setResources] = useState([]);
const [selectedDate, setSelectedDate] = useState(new Date());
const [sorting, setSorting] = useState({key: '', order: ''});
// Multi-column sorting: array of {key, order} objects
// First item has highest priority, subsequent items are secondary sorts
const [sortingRules, setSortingRules] = useState([]);
const [selectedItems, setSelectedItems] = useState([]);
const [showTransportationModal, setShowTransportationModal] = useState(false);
const [driver, setDriver] = useState(null);
@ -45,7 +47,7 @@ const EventsList = () => {
},
{
key: 'doctor',
label: 'Doctor',
label: 'Provider',
show: true
},
{
@ -60,7 +62,7 @@ const EventsList = () => {
},
{
key: 'translation',
label: 'Translation',
label: 'Language Support',
show: true
},
{
@ -70,7 +72,7 @@ const EventsList = () => {
},
{
key: 'needId',
label: 'Need ID',
label: 'ID Needed',
show: true
},
{
@ -80,17 +82,17 @@ const EventsList = () => {
},
{
key: 'startTime',
label: 'Start Time',
label: 'Appointment Time',
show: true
},
{
key: 'fasting',
label: 'Fasting',
label: 'Fasting Required',
show: true
},
{
key: 'transportation',
label: 'Driver',
label: 'Transportation Support',
show: true
}
]);
@ -100,6 +102,39 @@ const EventsList = () => {
return currentCustomer?.disability || event?.data?.disability?.toLowerCase() === 'yes' || false;
};
// Default sort: 1) driver name ascending, 2) start time early to late, 3) address ascending, 4) language (empty first)
const applyDefaultSort = (eventsArray) => {
return [...eventsArray].sort((a, b) => {
// 1. First sort by driver (transportation) name ascending
const driverA = (a.transportation || '').toLowerCase();
const driverB = (b.transportation || '').toLowerCase();
if (driverA !== driverB) {
return driverA.localeCompare(driverB);
}
// 2. Then sort by start time (early to late)
const timeA = a.start_time ? new Date(a.start_time).getTime() : 0;
const timeB = b.start_time ? new Date(b.start_time).getTime() : 0;
if (timeA !== timeB) {
return timeA - timeB;
}
// 3. Then sort by resource address ascending
const addressA = (a.address || '').toLowerCase();
const addressB = (b.address || '').toLowerCase();
if (addressA !== addressB) {
return addressA.localeCompare(addressB);
}
// 4. Finally sort by language (empty values first, then values with content)
const langA = (a.translation || '').trim();
const langB = (b.translation || '').trim();
if (langA === '' && langB !== '') return -1; // Empty comes first
if (langA !== '' && langB === '') return 1; // Empty comes first
return langA.localeCompare(langB); // Both empty or both have values - sort alphabetically
});
};
useEffect(() => {
if (!AuthService.canAccessLegacySystem()) {
window.alert('You haven\'t login yet OR this user does not have access to this page. Please change an admin account to login.')
@ -121,7 +156,7 @@ const EventsList = () => {
useEffect(() => {
if (customers?.length > 0 && resources?.length>0) {
EventsService.getAllEvents({ date: EventsService.formatDate(selectedDate) }).then((data) => {
setEvents(data.data.filter((item) => {
const processedEvents = data.data.filter((item) => {
item.customer = item?.data?.customer ? (customers.find(c=>c.id === item?.data?.customer)?.name || item?.data?.client_name || '') : (item?.data?.client_name || '');
item.doctor = item?.data?.resource ? (resources.find(r=> r.id === item?.data?.resource)?.name || item?.data?.resource_name || '') : (item?.data?.resource_name || '');
item.phone = item?.data?.resource ? (resources.find(r=> r.id === item?.data?.resource)?.phone || item?.data?.resource_phone || '') : (item?.data?.resource_phone || '');
@ -139,7 +174,11 @@ const EventsList = () => {
item.dob = item?.data?.customer ? customers.find(c => c.id === item?.data?.customer)?.birth_date : (item?.data?.client_birth_date || '');
item.transMethod = item?.data?.trans_method;
return item;
}).filter(item => item.type === 'medical' && item.confirmed));
}).filter(item => item.type === 'medical' && item.confirmed);
// Apply default sort (driver name → start time → address)
const sortedEvents = applyDefaultSort(processedEvents);
setEvents(sortedEvents);
setTransportationOptionsList(data.data.filter((item) => item.type === 'transportation' && item.status === 'active'))
})
}
@ -147,15 +186,40 @@ const EventsList = () => {
useEffect(() => {
const newEvents = [...events];
const sortedEvents = sorting.key === '' ? newEvents : newEvents.sort((a, b) => {
return a[sorting.key].localeCompare(b[sorting.key]);
// Apply multi-column sorting
// After all custom sorting rules, always sort by language (empty first)
const applyMultiColumnSort = (eventsArray, rules) => {
if (rules.length === 0) {
return applyDefaultSort(eventsArray);
}
return [...eventsArray].sort((a, b) => {
// First apply all custom sorting rules
for (const rule of rules) {
const valA = (a[rule.key] || '').toString().toLowerCase();
const valB = (b[rule.key] || '').toString().toLowerCase();
const comparison = valA.localeCompare(valB);
if (comparison !== 0) {
return rule.order === 'desc' ? -comparison : comparison;
}
}
// After all rules, always sort by language (empty values first, then values with content)
const langA = (a.translation || '').trim();
const langB = (b.translation || '').trim();
if (langA === '' && langB !== '') return -1; // Empty comes first
if (langA !== '' && langB === '') return 1; // Empty comes first
return langA.localeCompare(langB); // Both empty or both have values - sort alphabetically
});
setEvents(
sorting.order === 'asc' ? sortedEvents : sortedEvents.reverse()
)
}, [sorting]);
};
useEffect(() => {
if (events.length === 0) return;
const sortedEvents = applyMultiColumnSort(events, sortingRules);
setEvents(sortedEvents);
}, [sortingRules]);
const redirectToAdmin = () => {
@ -199,24 +263,46 @@ const EventsList = () => {
setShowDeletedItems(value === 'archivedEvents');
// Recover all filters
// setKeyword('');
setSorting({key: '', order: ''});
setSortingRules([]);
setSelectedItems([]);
}
// Get the sort indicator for a column (shows priority number if multi-column)
const getSortingImg = (key) => {
return sorting.key === key ? (sorting.order === 'asc' ? 'up_arrow' : 'down_arrow') : 'default';
const ruleIndex = sortingRules.findIndex(rule => rule.key === key);
if (ruleIndex === -1) {
return 'default';
}
return sortingRules[ruleIndex].order === 'asc' ? 'up_arrow' : 'down_arrow';
}
// Get the sort priority number for display (1, 2, 3, etc.)
const getSortPriority = (key) => {
const ruleIndex = sortingRules.findIndex(rule => rule.key === key);
return ruleIndex === -1 ? null : ruleIndex + 1;
}
// Multi-column sort: clicking a column adds it to sort rules or toggles its order
// Behavior: asc → desc → remove from sorting
const sortTableWithField = (key) => {
let newSorting = {
key,
order: 'asc',
}
const existingIndex = sortingRules.findIndex(rule => rule.key === key);
if (sorting.key === key && sorting.order === 'asc') {
newSorting = {...newSorting, order: 'desc'};
if (existingIndex === -1) {
// Column not in sort rules - add it with 'asc' order
setSortingRules([...sortingRules, { key, order: 'asc' }]);
} else {
const existingRule = sortingRules[existingIndex];
if (existingRule.order === 'asc') {
// Toggle to desc
const newRules = [...sortingRules];
newRules[existingIndex] = { key, order: 'desc' };
setSortingRules(newRules);
} else {
// Already desc - remove from sorting rules
const newRules = sortingRules.filter((_, index) => index !== existingIndex);
setSortingRules(newRules);
}
}
setSorting(newSorting);
}
const toggleSelectedAllItems = () => {
@ -308,13 +394,10 @@ const EventsList = () => {
item.transMethod = item?.data?.trans_method;
return item;
}).filter(item => item.type === 'medical' && item.confirmed);
const newEvents = [...eventsResults];
const sortedEvents = sorting.key === '' ? newEvents : newEvents.sort((a, b) => {
return a[sorting.key].localeCompare(b[sorting.key]);
});
setEvents(
sorting.order === 'asc' ? sortedEvents : sortedEvents.reverse()
)
// Apply current sorting rules (or default if none)
const sortedEvents = applyMultiColumnSort(eventsResults, sortingRules);
setEvents(sortedEvents);
setTransportationOptionsList(data.data.filter((item) => item.type === 'transportation' && item.status === 'active'));
closePanel();
})
@ -349,13 +432,10 @@ const EventsList = () => {
item.transMethod = item?.data?.trans_method;
return item;
}).filter(item => item.type === 'medical' && item.confirmed);
const newEvents = [...eventsResults];
const sortedEvents = sorting.key === '' ? newEvents : newEvents.sort((a, b) => {
return a[sorting.key].localeCompare(b[sorting.key]);
});
setEvents(
sorting.order === 'asc' ? sortedEvents : sortedEvents.reverse()
)
// Apply current sorting rules (or default if none)
const sortedEvents = applyMultiColumnSort(eventsResults, sortingRules);
setEvents(sortedEvents);
// setTransportationOptionsList(data.data.filter((item) => item.type === 'transportation' && item.status === 'active'));
closePanel();
})
@ -373,7 +453,11 @@ const EventsList = () => {
<th className="th-index">No.</th>
{
columns.filter(col => col.show).map((column, index) => <th className="sortable-header" key={index}>
{column.label} <span className="float-right" onClick={() => sortTableWithField(column.key)}><img src={`/images/${getSortingImg(column.key)}.png`}></img></span>
{column.label}
<span className="float-right" onClick={() => sortTableWithField(column.key)} style={{ cursor: 'pointer' }}>
{getSortPriority(column.key) && <small style={{ marginRight: '2px', fontSize: '10px', color: '#666' }}>{getSortPriority(column.key)}</small>}
<img src={`/images/${getSortingImg(column.key)}.png`}></img>
</span>
</th>)
}
<th>Customer Date of Birth</th>
@ -413,7 +497,7 @@ const EventsList = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Medical</Breadcrumb.Item>
<Breadcrumb.Item href="/medical/index">Medical</Breadcrumb.Item>
<Breadcrumb.Item active>
Appointment Information
</Breadcrumb.Item>
@ -426,7 +510,7 @@ const EventsList = () => {
</h4>
</div>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-container list-page">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="activeEvents" id="requests-tab" onSelect={(k) => showDeleted(k)}>
<Tab eventKey="activeEvents" title="Active Appointments">

View File

@ -375,7 +375,8 @@ const EventsMultipleList = () => {
control: (baseStyles, state) => ({
...baseStyles,
width: '210px',
height: '35px',
height: '45px',
minHeight: '45px',
'padding-top': 0,
'padding-bottom': 0,
'margin-top': 0,
@ -383,23 +384,27 @@ const EventsMultipleList = () => {
}),
indicatorSeparator: (baseStyles, state) => ({
...baseStyles,
width: 0
display: 'none'
}),
dropdownIndicator: (baseStyles) => ({
...baseStyles,
paddingRight: '12px'
}),
indicatorsContainer: (baseStyles) => ({
...baseStyles,
'margin-top': '-20px'
'margin-top': '-5px'
}),
placeholder: (baseStyles) => ({
...baseStyles,
'margin-top': '-20px',
'margin-top': '-5px',
'font-size': '13px'
}),
singleValue: (baseStyles, state) => ({
...baseStyles,
'margin-top': '-20px',
'margin-top': '-5px',
'font-size': '13px'
})
}} value={selectedCustomer || ''} onChange={selectedData => {setSelectedCustomer(selectedData); setSelectedItems([]);}} options={[{value: '', label: ''}, ...customers.map(resource => ({
}} value={selectedCustomer || ''} onChange={selectedData => {setSelectedCustomer(selectedData); setSelectedItems([]);}} options={[{value: '', label: ''}, ...customers.filter(customer => customer?.status === 'active' && customer?.type !== 'discharged').map(resource => ({
value: resource?.id || '',
label: resource?.name || '',
}))]}></Select>
@ -412,7 +417,8 @@ const EventsMultipleList = () => {
control: (baseStyles, state) => ({
...baseStyles,
width: '210px',
height: '25px',
height: '45px',
minHeight: '45px',
'padding-top': 0,
'padding-bottom': 0,
'margin-top': 0,
@ -420,11 +426,15 @@ const EventsMultipleList = () => {
}),
indicatorSeparator: (baseStyles, state) => ({
...baseStyles,
width: 0
display: 'none'
}),
dropdownIndicator: (baseStyles) => ({
...baseStyles,
paddingRight: '12px'
}),
indicatorsContainer: (baseStyles) => ({
...baseStyles,
'margin-top': '-20px'
'margin-top': '-5px'
}),
placeholder: (baseStyles) => ({
...baseStyles,
@ -458,7 +468,7 @@ const EventsMultipleList = () => {
<>
<div className="list row mb-4 noprint">
<Breadcrumb>
<Breadcrumb.Item>Medical</Breadcrumb.Item>
<Breadcrumb.Item href="/medical/index">Medical</Breadcrumb.Item>
<Breadcrumb.Item active>
Appointment Information (Multi Days)
</Breadcrumb.Item>
@ -472,7 +482,7 @@ const EventsMultipleList = () => {
</div>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-container list-page">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="activeEvents" id="requests-tab" onSelect={(k) => showDeleted(k)}>
<Tab eventKey="activeEvents" title="Active Appointments">
@ -485,10 +495,6 @@ const EventsMultipleList = () => {
<div className="field-label">To</div>
<DatePicker selected={toDate} onChange={(v) => {setToDate(v); setSelectedItems([]);}} />
</div>
<button className="btn btn-primary btn-sm me-2" onClick={() => {generateMedicalNotificationDocs()}}>Medical Notifications Doc</button>
<button className="btn btn-primary btn-sm me-2" onClick={() => {generateMedicalNotificationPdf()}}>Medical Notifications Pdf</button>
<button className="btn btn-primary btn-sm me-2" onClick={() => {generateVisitRecordSheet()}}>Visit Record Sheet</button>
<button className="btn btn-primary btn-sm me-2" onClick={() => {generateVisitRecordPDF()}}>Visit Record PDF</button>
</div>
{table('active')}
</Tab>
@ -502,10 +508,6 @@ const EventsMultipleList = () => {
<div className="field-label">To</div>
<DatePicker selected={toDate} onChange={(v) => {setToDate(v); setSelectedItems([]);}} />
</div>
<button className="btn btn-primary btn-sm me-2" onClick={() => {generateMedicalNotificationDocs()}}>Medical Notifications Doc</button>
<button className="btn btn-primary btn-sm me-2" onClick={() => {generateMedicalNotificationPdf()}}>Medical Notifications Pdf</button>
<button className="btn btn-primary btn-sm me-2" onClick={() => {generateVisitRecordSheet()}}>Visit Record Sheet</button>
<button className="btn btn-primary btn-sm me-2" onClick={() => {generateVisitRecordPDF()}}>Visit Record PDF</button>
</div>
{table('inactive')}
</Tab>
@ -530,7 +532,13 @@ const EventsMultipleList = () => {
<Export
columns={columns}
data={filteredEvents.filter(event => event.status === (showDeletedItems ? 'inactive' : 'active'))}
filename="events-multiple"
filename="events-multiple"
customActions={[
{ label: 'Medical Notifications Doc', onClick: generateMedicalNotificationDocs },
{ label: 'Medical Notifications Pdf', onClick: generateMedicalNotificationPdf },
{ label: 'Visit Record Sheet', onClick: generateVisitRecordSheet },
{ label: 'Visit Record PDF', onClick: generateVisitRecordPDF }
]}
/>
</div>
</div>

View File

@ -1,6 +1,6 @@
import React, {useState, useEffect} from "react";
// import { useDispatch } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
// import { customerSlice } from "./../../store";
import { AuthService, CustomerService, EventsService, ResourceService } from "../../services";
import Select from 'react-select';
@ -11,6 +11,8 @@ import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab } from "react-bootstrap"
const UpdateEvent = () => {
const navigate = useNavigate();
const urlParams = useParams();
const [searchParams] = useSearchParams();
const [activeTab, setActiveTab] = useState(searchParams.get('tab') || 'eventInfo');
const [currentEvent, setCurrentEvent] = useState(undefined);
const [medicalResource, setMedicalResource] = useState(undefined);
const [currentResource, setCurrentResource] = useState(undefined);
@ -70,7 +72,32 @@ const UpdateEvent = () => {
navigate(`/medical/events/${urlParams.id}`);
}
const validateEvent = () => {
const errors = [];
// Required fields validation
if (!currentCustomer || !currentCustomer.id) {
errors.push('Customer');
}
if (!currentResource || !currentResource.id) {
errors.push('Provider');
}
if (!startTime) {
errors.push('Appointment Time');
}
if (errors.length > 0) {
window.alert(`Please fill in the following required fields:\n${errors.join('\n')}`);
return false;
}
return true;
};
const saveEvent = () => {
if (!validateEvent()) {
return;
}
let newEventDataClientAndResource = {};
let newEventClientAndResource = {};
if (currentCustomer) {
@ -266,8 +293,8 @@ const UpdateEvent = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Medical</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/medical/index">Medical</Breadcrumb.Item>
<Breadcrumb.Item href="/medical/events/list">
Medical Appointment Information
</Breadcrumb.Item>
<Breadcrumb.Item active>
@ -279,54 +306,62 @@ const UpdateEvent = () => {
</div>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-container form-page">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="eventInfo" id="event-tab">
{ !hideCreateFields && <Tab eventKey="eventClientInfo" title="Appointment Client And Time Information">
<Tabs activeKey={activeTab} onSelect={(k) => setActiveTab(k)} id="event-tab">
{ !hideCreateFields && <Tab eventKey="eventClientInfo" title="Appointment Customer And Time Information">
<div className="multi-columns-container">
<div className="column-container">
<div className="column-card">
<h6 className="text-primary">Client And Start Time</h6>
<h6 className="text-primary">Customer And Appointment Time</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Client
<div className="field-label">Customer
<span className="required">*</span>
</div>
<Select styles={{
control: (baseStyles, state) => ({
control: (baseStyles) => ({
...baseStyles,
width: '350px',
height: '45px',
'padding-top': 0,
'padding-bottom': 0,
'margin-top': 0,
'margin-bottom': 0
borderRadius: '8px'
}),
indicatorSeparator: (baseStyles, state) => ({
...baseStyles,
width: 0
indicatorSeparator: () => ({
display: 'none'
}),
indicatorsContainer: (baseStyles) => ({
dropdownIndicator: (baseStyles) => ({
...baseStyles,
'margin-top': '-10px'
paddingRight: '8px',
color: '#333',
'&:hover': { color: '#000' }
}),
valueContainer: (baseStyles) => ({
...baseStyles,
height: '43px',
padding: '0 8px',
}),
input: (baseStyles) => ({
...baseStyles,
margin: '0px',
padding: '0px',
height: '30px',
width: '290px'
}),
placeholder: (baseStyles) => ({
...baseStyles,
'margin-top': '-10px',
'font-size': '13px'
fontSize: '13px'
}),
singleValue: (baseStyles, state) => ({
singleValue: (baseStyles) => ({
...baseStyles,
'margin-top': '-10px',
'font-size': '13px'
margin: '0px',
fontSize: '13px'
})
}} value={customerName || ''} onChange={selectedCustomer => onCustomerChange(selectedCustomer)} options={[{value: '', label: ''}, ...customers.map(customer => ({
}} value={customerName || ''} onChange={selectedCustomer => onCustomerChange(selectedCustomer)} options={[{value: '', label: ''}, ...customers.filter(customer => customer?.status === 'active' && customer?.type !== 'discharged').map(customer => ({
value: customer?.id || '',
label: customer?.name || ''
}))]}></Select>
</div>
<div className="me-4">
<div className="field-label">Start Time
<div className="field-label">Appointment Time
<span className="required">*</span>
</div>
<DatePicker
@ -350,10 +385,10 @@ const UpdateEvent = () => {
<div className="column-card">
{
currentCustomer && <>
<h6 className="text-primary">Client Information</h6>
<h6 className="text-primary">Customer Information</h6>
<div className="app-main-content-fields-section short">
<div className="field-body">
<div className="field-label">Client Name</div>
<div className="field-label">Customer Name</div>
<div className="field-value">{currentCustomer?.name || ''}</div>
</div>
<div className="field-body">
@ -392,23 +427,15 @@ const UpdateEvent = () => {
<h6 className="text-primary">Appointment Details</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Resource
<div className="field-label">Provider
<span className="required">*</span>
</div>
{currentResource ?
(<><span>{currentResource?.name}</span> <span><button className="btn btn-link btn-sm me-2 mb-2" onClick={()=> setShowResourceModal(true)}>Update</button></span></>) :
(currentEvent?.data?.resource_name ? <><span>{currentEvent?.data?.resource_name}</span> <span><button className="btn btn-link btn-sm me-2 mb-2" onClick={()=> setShowResourceModal(true)}>Update</button></span></> : <button className="btn btn-link btn-sm me-2 mb-2" onClick={()=> setShowResourceModal(true)}>Please click here to add resources</button>)}
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Title
<span className="required">*</span>
</div>
<input type="text" value={title || ''} onChange={e => setTitle(e.target.value)}/>
(currentEvent?.data?.resource_name ? <><span>{currentEvent?.data?.resource_name}</span> <span><button className="btn btn-link btn-sm me-2 mb-2" onClick={()=> setShowResourceModal(true)}>Update</button></span></> : <button className="btn btn-link btn-sm me-2 mb-2" onClick={()=> setShowResourceModal(true)}>Please click here to add provider</button>)}
</div>
<div className="me-4">
<div className="field-label">Tag
<div className="field-label">Label
<span className="required">*</span>
</div>
<select value={color} onChange={e => setColor(e.target.value)}>
@ -451,7 +478,7 @@ const UpdateEvent = () => {
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Need Id
<div className="field-label">ID Needed
</div>
<select value={needId} onChange={e => setNeedId(e.target.value)}>
<option value=""></option>
@ -483,16 +510,13 @@ const UpdateEvent = () => {
</select>
</div>
<div className="me-4">
<div className="field-label">Transportation Needed
<div className="field-label">Transportation Support
</div>
<select value={transMethod} onChange={e => setTransMethod(e.target.value)}>
<option value=""></option>
<option value="by own">By Own</option>
<option value="televisit">Televisit</option>
<option value="pickup only">Pickup Only</option>
<option value="dropoff only">Dropoff Only</option>
<option value="client does not need to go">Client Does Not Need To Go</option>
<option value="Center Transportation">By Center Transportation</option>
{
EventsService.transportationTypeOptions?.map((item) => <option key={item?.value} value={item?.value}>{item?.label}</option>)
}
</select>
</div>
</div>
@ -563,7 +587,7 @@ const UpdateEvent = () => {
</div>
<Modal show={showResourceModal} fullscreen={'xxl-down'} onHide={() => setShowResourceModal(false)}>
<Modal.Header closeButton>
<Modal.Title>Select the Resource</Modal.Title>
<Modal.Title>Select the Provider</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="app-main-content-fields-section">
@ -572,7 +596,7 @@ const UpdateEvent = () => {
<input type="text" value={keyword} onChange={e => setKeyword(e.target.value)}/>
</div>
<div className="mb-4 me-4">
<div className="field-label">Filter By Resource Type</div>
<div className="field-label">Filter By Provider Type</div>
<select value={resourceType} onChange={e => setResourceType(e.target.value)}>
<option value=""></option>
<option value="doctor">Doctor</option>

View File

@ -1,132 +1,209 @@
import React, {useState, useEffect} from "react";
// import { useDispatch } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
// import { customerSlice } from "./../../store";
import { PencilSquare } from "react-bootstrap-icons";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { AuthService, CustomerService, EventsService, ResourceService } from "../../services";
import { Breadcrumb, Tabs, Tab } from "react-bootstrap";
const ViewEvent = () => {
const navigate = useNavigate();
const urlParams = useParams();
const [searchParams] = useSearchParams();
const [activeTab, setActiveTab] = useState(searchParams.get('tab') || 'eventInfo');
const [currentEvent, setCurrentEvent] = useState(undefined);
const [resources, setResources] = useState([]);
const [customers, setCustomers] = useState([]);
const params = new URLSearchParams(window.location.search);
const redirectToCalendar = () => {
navigate(`/medical/events/calendar`);
navigate(`/medical/events/calendar`);
}
const redirectTo = (id) => {
const redirectTo = () => {
navigate(`/medical/events/list`);
}
const goToEdit = (id) => {
navigate(`/medical/events/edit/${id}?tab=${activeTab}`);
}
useEffect(() => {
if (!AuthService.canAccessLegacySystem()) {
window.alert('You haven\'t login yet OR this user does not have access to this page. Please change an admin account to login.')
AuthService.logout();
navigate(`/login`);
}
Promise.all([ResourceService.getAll(), CustomerService.getAllCustomers()]).then(([resourcesData, customersData]) => {
Promise.all([ResourceService.getAll(), CustomerService.getAllActiveCustomers()]).then(([resourcesData, customersData]) => {
setResources(resourcesData.data);
setCustomers(customersData.data);
if (!currentEvent) {
EventsService.getEvent(urlParams.id).then(eventData => {
setCurrentEvent(eventData.data);
})
}
})
}, []);
const getResourceName = () => {
if (currentEvent?.data?.resource) {
return resources.find(r => r.id === currentEvent?.data?.resource)?.name || currentEvent?.data?.resource_name;
}
return currentEvent?.data?.resource_name || '';
};
const getCustomerName = () => {
if (currentEvent?.data?.customer) {
return customers.find(c => c.id === currentEvent?.data?.customer)?.name || currentEvent?.data?.client_name;
}
return currentEvent?.data?.client_name || '';
};
const formatDateTime = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
};
return (
<>
<div className="list row">
<div className="col-md-12 col-xs-12">
<div className="list row mb-4">
<div className="col-md-12 text-primary">
<h5>View Medical Event Details<button className="btn btn-link btn-sm" onClick={() => {redirectTo()}}>Back</button></h5>
</div>
</div>
<div className="list row mb-4">
<div className="col-md-6 mb-4">
<div>Resource:</div>
{ currentEvent?.data?.resource ? (resources.find(r => r.id === currentEvent?.data?.resource)?.name || currentEvent?.data?.resource_name) : currentEvent?.data?.resource_name}
</div>
<div className="col-md-6 mb-4">
<div>Client:</div>
{ currentEvent?.data?.customer ? (customers.find(r => r.id === currentEvent?.data?.customer)?.name || currentEvent?.data?.client_name) : currentEvent?.data?.client_name}
</div>
<div className="col-md-6 mb-4">
<div>Title:</div> {currentEvent?.title}
</div>
<div className="col-md-6 mb-4">
<div>Start Time:</div> {`${new Date(currentEvent?.start_time)?.toLocaleDateString()} ${new Date(currentEvent?.start_time)?.toLocaleTimeString()}`}
</div>
{/* <div className="col-md-12 mb-4">
<div>Description:</div> {currentEvent?.description}
</div> */}
<div className="col-md-4 mb-4">
<div>Color:</div>
{currentEvent?.color || ''}
</div>
<div className="col-md-4 mb-4">
<div>New Patient:</div>
{currentEvent?.data?.new_patient}
</div>
<div className="col-md-4 mb-4">
<div>Confirmed:</div>
{currentEvent?.data?.confirmed}
</div>
<div className="col-md-4 mb-4">
<div>Fasting:</div>
{currentEvent?.data?.fasting}
</div>
<div className="col-md-4 mb-4">
<div>Interpreter Level:</div>
{currentEvent?.data?.interpreter}
</div>
<div className="col-md-4 mb-4">
<div>Doctor Order:</div>
{currentEvent?.data?.doc_order}
</div>
<div className="col-md-4 mb-4">
<div>Disability: </div>
{currentEvent?.data?.disability}
</div>
<div className="col-md-4 mb-4">
<div>Need Id: </div>
{currentEvent?.data?.need_id}
</div>
<div className="col-md-4 mb-4">
<div>Need Medication List</div>
{currentEvent?.data?.need_med_list}
</div>
<div className="col-md-4 mb-4">
<div>Disability Support:</div>
{currentEvent?.data?.disability_support}
</div>
<div className="col-md-4 mb-4">
<div>Transportation:</div>
{currentEvent?.data?.trans_method}
</div>
<div className="col-md-4 mb-4">
<div>Notes:</div>
{currentEvent?.data?.notes}
</div>
<div className="col-md-4 mb-4">
<div>Reason:</div>
{currentEvent?.data?.reason}
</div>
<div className="col-md-4 mb-4">
<div>Other Requirements:</div>
{currentEvent?.data?.other}
</div>
</div>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item href="/medical/index">Medical</Breadcrumb.Item>
<Breadcrumb.Item href="/medical/events/list">
Medical Events
</Breadcrumb.Item>
<Breadcrumb.Item active>
View Event Details
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>View Medical Event Details <button className="btn btn-link btn-sm" onClick={() => {redirectTo()}}>Back</button></h4>
</div>
</div>
<div className="app-main-content-list-container form-page">
<div className="app-main-content-list-func-container">
<Tabs activeKey={activeTab} onSelect={(k) => setActiveTab(k)} id="event-view-tab">
<Tab eventKey="eventInfo" title="Event Information">
<h6 className="text-primary">Appointment Details</h6>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Provider</div>
<div className="field-value">{getResourceName()}</div>
</div>
<div className="field-body">
<div className="field-label">Customer</div>
<div className="field-value">{getCustomerName()}</div>
</div>
<div className="field-body">
<div className="field-label">Label</div>
<div className="field-value">
{currentEvent?.color && (
<span style={{
display: 'inline-block',
width: '16px',
height: '16px',
borderRadius: '4px',
backgroundColor: currentEvent?.color,
marginRight: '8px',
verticalAlign: 'middle'
}}></span>
)}
{currentEvent?.color || ''}
</div>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Appointment Time</div>
<div className="field-value">{formatDateTime(currentEvent?.start_time)}</div>
</div>
<div className="field-body">
<div className="field-label">End Time</div>
<div className="field-value">{formatDateTime(currentEvent?.stop_time)}</div>
</div>
</div>
<h6 className="text-primary">Patient Information</h6>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">New Patient</div>
<div className="field-value">{currentEvent?.data?.new_patient || 'No'}</div>
</div>
<div className="field-body">
<div className="field-label">Confirmed</div>
<div className="field-value">{currentEvent?.confirmed ? 'Yes' : 'No'}</div>
</div>
<div className="field-body">
<div className="field-label">Fasting Required</div>
<div className="field-value">{currentEvent?.data?.fasting || 'No'}</div>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Language Support</div>
<div className="field-value">{currentEvent?.data?.interpreter || ''}</div>
</div>
<div className="field-body">
<div className="field-label">Eyes-on Required</div>
<div className="field-value">{currentEvent?.data?.disability || 'No'}</div>
</div>
<div className="field-body">
<div className="field-label">ID Needed</div>
<div className="field-value">{currentEvent?.data?.need_id || 'No'}</div>
</div>
</div>
<h6 className="text-primary">Medical Requirements</h6>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Doctor Order</div>
<div className="field-value">{currentEvent?.data?.doc_order || ''}</div>
</div>
<div className="field-body">
<div className="field-label">Need Medication List</div>
<div className="field-value">{currentEvent?.data?.need_med_list || 'No'}</div>
</div>
<div className="field-body">
<div className="field-label">Disability Support</div>
<div className="field-value">{currentEvent?.data?.disability_support || ''}</div>
</div>
</div>
<h6 className="text-primary">Transportation & Notes</h6>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Transportation Support</div>
<div className="field-value">{currentEvent?.data?.trans_method || ''}</div>
</div>
<div className="field-body">
<div className="field-label">Reason</div>
<div className="field-value">{currentEvent?.data?.reason || ''}</div>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Notes</div>
<div className="field-value">{currentEvent?.data?.notes || ''}</div>
</div>
<div className="field-body">
<div className="field-label">Other Requirements</div>
<div className="field-value">{currentEvent?.data?.other || ''}</div>
</div>
</div>
</Tab>
</Tabs>
<div className="list-func-panel">
<button className="btn btn-primary" onClick={() => goToEdit(currentEvent?.id)}>
<PencilSquare className="me-2" size={16}></PencilSquare>Edit
</button>
</div>
</div>
</div>
</>
);
};
export default ViewEvent;
export default ViewEvent;

View File

@ -131,15 +131,27 @@ const SideMenu = () => {
{
name: 'Transportation Schedule',
link: '/trans-routes/dashboard',
category: '/trans-routes',
category: '/trans-routes/dashboard',
roleFunc: AuthService.canViewRoutes
},
{
name: 'Customer Report',
link: '/admin/customer-report',
category: '/admin/',
roleFunc: AuthService.canViewAttendance
}
name: 'Templates',
link: '/trans-routes/daily-templates/list',
category: '/trans-routes/daily-templates',
roleFunc: AuthService.canViewRoutes
},
{
name: 'Appointment One-Day List',
link: '/medical/events/list',
category: '/events/list',
roleFunc: AuthService.canAccessLegacySystem
},
// {
// name: 'Customer Report',
// link: '/admin/customer-report',
// category: '/admin/',
// roleFunc: AuthService.canViewAttendance
// }
// {
// name: 'Schedule Driver for Appointment',
// link: '#',
@ -171,12 +183,6 @@ const SideMenu = () => {
category: '/events/calendar',
roleFunc: AuthService.canAccessLegacySystem
},
{
name: 'Appointment One-Day List',
link: '/medical/events/list',
category: '/events/list',
roleFunc: AuthService.canAccessLegacySystem
},
{
name: 'Appointment Multi-Days List',
link: '/medical/events/multiple-list',
@ -191,11 +197,11 @@ const SideMenu = () => {
collapsed: false,
roleFunc: () => true,
subNavs: [
// {
// name: 'Meal Status',
// link: '#',
// roleFunc: () => true
// },
{
name: 'Meal Status',
link: '/meal-status',
roleFunc: () => true
},
{
name: 'Seating Chart',
link: '/seating',

View File

@ -122,9 +122,12 @@ const InfoScreen = () => {
console.log('Attendance note image response:', imageResponse);
if (imageResponse.data && imageResponse.data.files && imageResponse.data.files.length > 0) {
// Handle both response.data.files and response.data.data.files structures
const files = imageResponse?.data?.data?.files || imageResponse?.data?.files;
if (files && Array.isArray(files) && files.length > 0) {
// Get the first image file
const imageFile = imageResponse.data.files[0];
const imageFile = files[0];
console.log('Attendance note image file:', imageFile);
setAttendanceNote(prev => ({
...prev,
@ -176,12 +179,18 @@ const InfoScreen = () => {
'carousel_item'
);
console.log('Carousel images response:', imagesResponse);
console.log('imagesResponse.data:', imagesResponse?.data);
console.log('imagesResponse.data?.data:', imagesResponse?.data?.data);
console.log('imagesResponse.data?.files:', imagesResponse?.data?.files);
if (imagesResponse.data && imagesResponse.data.files && imagesResponse.data.files.length > 0) {
// Handle both response.data.files and response.data.data.files structures
const files = imagesResponse?.data?.data?.files || imagesResponse?.data?.files;
if (files && Array.isArray(files) && files.length > 0) {
// Get all image files and extract URLs
const imageUrls = imagesResponse.data.files.map(file => file.url);
const imageUrls = files.map(file => file.url);
console.log('Carousel image URLs:', imageUrls);
console.log('Carousel files array:', imagesResponse.data.files);
console.log('Carousel files array:', files);
setCarousel(prev => ({
...prev,
images: imageUrls
@ -189,6 +198,12 @@ const InfoScreen = () => {
} else {
console.log('No files found in carousel response');
console.log('Response structure:', imagesResponse);
console.log('files value:', files);
console.log('files type:', typeof files);
console.log('files isArray:', Array.isArray(files));
if (files) {
console.log('files length:', files.length);
}
}
} catch (imageError) {
console.log('No images found for carousel:', imageError);
@ -910,61 +925,50 @@ const InfoScreen = () => {
{/* Full Screen Content */}
{isFullScreen && (
<div className="multi-columns-container">
{/* Column 1 - Appointments */}
<div className="column-container">
<div className="column-card" style={{ backgroundColor: 'rgba(255, 255, 255, 0.8)' }}>
<h6 className="text-black" style={{ fontSize: '14px', fontWeight: '600' }}>Appointments</h6>
<div className="app-main-content-fields-section">
<div className="list row mb-4">
<div className="col-md-12">
<table className="personnel-info-table info-screen-appointments-table">
<thead>
<tr>
<th>Time</th>
<th>Customer</th>
<th>Location</th>
</tr>
</thead>
<tbody>
{medicalEvents && medicalEvents.length > 0 ? (
medicalEvents.map((medicalEvent, index) => (
<tr key={medicalEvent.id}>
<td>{medicalEvent.startTime}</td>
<td>{medicalEvent.customer}</td>
<td>{medicalEvent.location}</td>
</tr>
))
) : (
<tr>
<td colSpan="3" className="text-center">No medical appointments scheduled for today</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
<div className="column-card">
<h6 className="text-black fullscreen-title">Appointments</h6>
<table className="personnel-info-table info-screen-appointments-table">
<thead>
<tr>
<th>Time</th>
<th>Customer</th>
<th>Location</th>
</tr>
</thead>
<tbody>
{medicalEvents && medicalEvents.length > 0 ? (
medicalEvents.map((medicalEvent, index) => (
<tr key={medicalEvent.id}>
<td>{medicalEvent.startTime}</td>
<td>{medicalEvent.customer}</td>
<td>{medicalEvent.location}</td>
</tr>
))
) : (
<tr>
<td colSpan="3" className="text-center">No medical appointments scheduled for today</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* Column 2 - Time, Weather, Attendance Note, Gallery */}
<div className="column-container">
{/* First Piece - Time and Weather Cards */}
<div className="row mb-4">
{/* Time and Weather Cards Row */}
<div className="row mb-4 time-weather-row">
{/* Time Card */}
<div className="col-md-6">
<div className="card h-100" style={{borderRadius: '8px', backgroundColor: 'rgba(255, 255, 255, 0.8)'}}>
<div className="card h-100">
<div className="card-body text-center">
<div className="mb-3">
<div className="text-black" style={{ fontSize: '12px' }}>
{moment(currentTime).format('dddd')}
</div>
<div className="text-black">
{moment(currentTime).format('MMM Do, YYYY')}
</div>
<h5 className="text-primary mt-2" style={{ fontSize: '20px', fontWeight: '600' }}>
{moment(currentTime).format('HH:mm')}
</h5>
<div className="fullscreen-date-display">
<div className="text-black">{moment(currentTime).format('dddd')}</div>
<div className="text-black">{moment(currentTime).format('MMM Do, YYYY')}</div>
<h5 className="text-primary mt-2 fullscreen-time">{moment(currentTime).format('HH:mm')}</h5>
</div>
{/* CSS Clock */}
<div className="clock-container">
<div className="clock">
<div className="clock-face">
@ -987,78 +991,41 @@ const InfoScreen = () => {
{/* Weather Card */}
<div className="col-md-6">
<div className="card h-100" style={{borderRadius: '8px', backgroundColor: 'rgba(255, 255, 255, 0.8)'}}>
<div className="card h-100">
<div className="card-body text-center">
<div className="mb-3">
<div className="text-black" style={{ fontSize: '12px' }}>
New York, NY
</div>
<div className="text-black">
Partly Cloudy
</div>
<h5 className="text-primary mt-2" style={{ fontSize: '20px', fontWeight: '600' }}>
22°C
</h5>
<div className="fullscreen-date-display">
<div className="text-black">New York, NY</div>
<div className="text-black">Partly Cloudy</div>
<h5 className="text-primary mt-2 fullscreen-time">22°C</h5>
</div>
{/* Weather Icon */}
<div className="weather-icon">
<i className="fas fa-cloud-sun" style={{ fontSize: '48px', color: '#ffc107' }}></i>
<i className="fas fa-cloud-sun"></i>
</div>
</div>
</div>
</div>
</div>
{/* Second Piece - AttendanceNote Card */}
<div className="mb-4">
<div className="card" style={{borderRadius: '8px', maxWidth: '450px', backgroundColor: 'rgba(255, 255, 255, 0.8)'}}>
{/* AttendanceNote Card */}
<div className="attendance-note-wrapper">
<div className="card">
<div className="card-body">
{/* Header with Logo and Edit Button */}
<div className="d-flex justify-content-between align-items-start mb-3">
<div className="d-flex justify-content-between align-items-start mb-2">
<div className="d-flex align-items-center">
<img src="/images/logo-trans.png" alt="Worldshine Logo" style={{ height: '30px', marginRight: '10px' }} />
<strong className="logo-worldshine" style={{ color: '#0066B1', fontSize: '16px' }}>Worldshine</strong>
<img src="/images/logo-trans.png" alt="Worldshine Logo" className="fullscreen-logo" />
<strong className="logo-worldshine">Worldshine</strong>
</div>
<Button
variant="link"
className="p-0"
onClick={handleEditClick}
style={{ color: '#666' }}
>
<PencilSquare size={16} />
</Button>
</div>
{/* Content Layout */}
<div className="row">
{/* Left Side - Text Content */}
<div className="col-md-8">
<h5 className="text-primary mb-2">{attendanceNote.slogan}</h5>
<p className="text-muted" style={{ fontSize: '14px', lineHeight: '1.6' }}>
{attendanceNote.introduction}
</p>
<p className="text-muted fullscreen-intro">{attendanceNote.introduction}</p>
</div>
{/* Right Side - Image */}
<div className="col-md-4">
{attendanceNote.image ? (
<img
src={attendanceNote.image}
alt="Attendance Note"
className="img-fluid rounded"
style={{ maxHeight: '120px', width: '100%', objectFit: 'cover' }}
/>
<img src={attendanceNote.image} alt="Attendance Note" className="img-fluid rounded fullscreen-note-img" />
) : (
<div
className="d-flex align-items-center justify-content-center"
style={{
height: '120px',
backgroundColor: '#f8f9fa',
border: '2px dashed #dee2e6',
borderRadius: '8px'
}}
>
<div className="d-flex align-items-center justify-content-center fullscreen-placeholder">
<span className="text-muted">No image</span>
</div>
)}
@ -1068,56 +1035,26 @@ const InfoScreen = () => {
</div>
</div>
{/* Third Piece - Gallery */}
<div className="mb-4">
<div className="card" style={{borderRadius: '8px', maxWidth: '450px', backgroundColor: 'rgba(255, 255, 255, 0.8)'}}>
{/* Gallery Card */}
<div className="gallery-wrapper">
<div className="card">
<div className="card-body">
{/* Header with Title and Edit Button */}
<div className="d-flex justify-content-between align-items-center mb-3">
<h6 className="text-black mb-0" style={{ fontSize: '14px', fontWeight: '500' }}>Gallery</h6>
<Button
variant="link"
className="p-0"
onClick={handleCarouselEditClick}
style={{ color: '#666' }}
>
<PencilSquare size={16} />
</Button>
</div>
{/* Image Carousel */}
<h6 className="text-black mb-2 fullscreen-title">Gallery</h6>
{carousel.images.length > 0 ? (
<Carousel
interval={10000}
interval={carousel.images.length > 1 ? 5000 : null}
controls={false}
indicators={true}
style={{ height: '200px' }}
indicators={carousel.images.length > 1}
className="fullscreen-carousel"
>
{carousel.images.map((image, index) => (
<Carousel.Item key={index}>
<img
className="d-block w-100"
src={image}
alt={`Gallery ${index + 1}`}
style={{
height: '200px',
objectFit: 'cover',
borderRadius: '4px'
}}
/>
<img className="d-block w-100 fullscreen-carousel-img" src={image} alt={`Gallery ${index + 1}`} />
</Carousel.Item>
))}
</Carousel>
) : (
<div
className="d-flex align-items-center justify-content-center"
style={{
height: '200px',
backgroundColor: '#f8f9fa',
border: '2px dashed #dee2e6',
borderRadius: '4px'
}}
>
<div className="d-flex align-items-center justify-content-center fullscreen-placeholder">
<span className="text-muted">No images in gallery</span>
</div>
)}
@ -1125,17 +1062,16 @@ const InfoScreen = () => {
</div>
</div>
</div>
{/* Column 3 - Activities and Menu */}
<div className="column-container">
<div className="column-card mb-4" style={{ backgroundColor: 'rgba(255, 255, 255, 0.8)' }}>
<h6 className="text-black mb-3" style={{ fontSize: '14px', fontWeight: '500' }}>Activities</h6>
{/* Activities Table */}
<div className="column-card">
<h6 className="text-black fullscreen-title">Activities</h6>
<table className="personnel-info-table info-screen-appointments-table">
<thead>
<tr>
<th>Time</th>
<th>Activity Name</th>
<th>Activity</th>
<th>Location</th>
</tr>
</thead>
@ -1155,40 +1091,33 @@ const InfoScreen = () => {
)}
</tbody>
</table>
</div>
<div className="column-card" style={{ backgroundColor: 'rgba(255, 255, 255, 0.8)' }}>
{/* Menu Table */}
<div className="list row mb-4">
<div className="col-md-12">
<h6 className="text-black mb-3" style={{ fontSize: '14px', fontWeight: '500' }}>Menu</h6>
<table className="personnel-info-table info-screen-appointments-table">
<thead>
<tr>
<th>Breakfast</th>
<th>Lunch</th>
<th>Snack</th>
<div className="column-card">
<h6 className="text-black fullscreen-title">Menu</h6>
<table className="personnel-info-table info-screen-appointments-table">
<thead>
<tr>
<th>Breakfast</th>
<th>Lunch</th>
<th>Snack</th>
</tr>
</thead>
<tbody>
{mealEvents && mealEvents.length > 0 ? (
mealEvents.map((mealEvent, index) => (
<tr key={mealEvent.id}>
<td>{mealEvent.breakfast}</td>
<td>{mealEvent.lunch}</td>
<td>{mealEvent.snack}</td>
</tr>
</thead>
<tbody>
{mealEvents && mealEvents.length > 0 ? (
mealEvents.map((mealEvent, index) => (
<tr key={mealEvent.id}>
<td>{mealEvent.breakfast}</td>
<td>{mealEvent.lunch}</td>
<td>{mealEvent.snack}</td>
</tr>
))
) : (
<tr>
<td colSpan="3" className="text-center">No meal plan for today</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
))
) : (
<tr>
<td colSpan="3" className="text-center">No meal plan for today</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>

View File

@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
const Landing = () => {
const navigate = useNavigate();
const goToAdmin = () => {
navigate('/admin');
navigate('/dashboard/dashboard');
}
const getLogoSuffix = () => {
@ -31,9 +31,9 @@ const Landing = () => {
<div className="landing-content mb-4">
<button onClick={() => goToAdmin()} className="btn btn-primary">Center Management Access</button>
</div>
<div className="landing-content">
{/* <div className="landing-content">
<button className="btn btn-primary">HR Management Access</button>
</div>
</div> */}
</div>
</div>
<div className="landing-img"/>

View File

@ -22,7 +22,29 @@ const CreateMessage = () => {
navigate(`/messages/list`);
}
const validateMessage = () => {
const errors = [];
// Required fields validation
if (!messageGroup || messageGroup === '') {
errors.push('Message Group');
}
if (!messageName || messageName.trim() === '') {
errors.push('Message Name');
}
if (errors.length > 0) {
window.alert(`Please fill in the following required fields:\n${errors.join('\n')}`);
return false;
}
return true;
};
const saveMessage = () => {
if (!validateMessage()) {
return;
}
const data = {
message_group: messageGroup,
message_title: messageTitle,
@ -38,10 +60,13 @@ const CreateMessage = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>General</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/">General</Breadcrumb.Item>
<Breadcrumb.Item href="/messages/list">
Messaging
</Breadcrumb.Item>
<Breadcrumb.Item active>
Create Message Template
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>

View File

@ -49,7 +49,7 @@ const MessageList = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>General</Breadcrumb.Item>
<Breadcrumb.Item href="/">General</Breadcrumb.Item>
<Breadcrumb.Item active>
Messaging
</Breadcrumb.Item>

View File

@ -58,7 +58,7 @@ const MessageTokenEditor = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>General</Breadcrumb.Item>
<Breadcrumb.Item href="/">General</Breadcrumb.Item>
<Breadcrumb.Item active>
Messaging
</Breadcrumb.Item>

View File

@ -67,8 +67,8 @@ const SendMessage = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>General</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/">General</Breadcrumb.Item>
<Breadcrumb.Item href="/messages/list">
Messaging
</Breadcrumb.Item>
<Breadcrumb.Item active>
@ -96,69 +96,85 @@ const SendMessage = () => {
<span className="required">*</span>
</div>
<Select styles={{
control: (baseStyles, state) => ({
control: (baseStyles) => ({
...baseStyles,
width: '350px',
height: '45px',
'padding-top': 0,
'padding-bottom': 0,
'margin-top': 0,
'margin-bottom': 0
borderRadius: '8px'
}),
indicatorSeparator: (baseStyles, state) => ({
...baseStyles,
width: 0
indicatorSeparator: () => ({
display: 'none'
}),
indicatorsContainer: (baseStyles) => ({
dropdownIndicator: (baseStyles) => ({
...baseStyles,
'margin-top': '-10px'
paddingRight: '8px',
color: '#333',
'&:hover': { color: '#000' }
}),
valueContainer: (baseStyles) => ({
...baseStyles,
height: '43px',
padding: '0 8px',
}),
input: (baseStyles) => ({
...baseStyles,
margin: '0px',
padding: '0px',
height: '30px',
width: '290px'
}),
placeholder: (baseStyles) => ({
...baseStyles,
'margin-top': '-10px',
'font-size': '13px'
fontSize: '13px'
}),
singleValue: (baseStyles, state) => ({
singleValue: (baseStyles) => ({
...baseStyles,
'margin-top': '-10px',
'font-size': '13px'
margin: '0px',
fontSize: '13px'
})
}} value={contactSeniorPhone || ''} onChange={selectedData => onContactSeniorChange(selectedData)} options={[{value: '', label: ''}, ...seniorPhoneList.map(senior => ({
value: senior?.mobile_phone || '',
label: `${senior?.name}(${senior?.name_cn}) - ${senior?.mobile_phone}` || '',
}))]}></Select>
</div>
</div>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Select Message Template (Optional)</div>
<Select styles={{
control: (baseStyles, state) => ({
control: (baseStyles) => ({
...baseStyles,
width: '350px',
height: '45px',
'padding-top': 0,
'padding-bottom': 0,
'margin-top': 0,
'margin-bottom': 0
borderRadius: '8px'
}),
indicatorSeparator: (baseStyles, state) => ({
...baseStyles,
width: 0
indicatorSeparator: () => ({
display: 'none'
}),
indicatorsContainer: (baseStyles) => ({
dropdownIndicator: (baseStyles) => ({
...baseStyles,
'margin-top': '-10px'
paddingRight: '8px',
color: '#333',
'&:hover': { color: '#000' }
}),
valueContainer: (baseStyles) => ({
...baseStyles,
height: '43px',
padding: '0 8px',
}),
input: (baseStyles) => ({
...baseStyles,
margin: '0px',
padding: '0px',
height: '30px',
width: '290px'
}),
placeholder: (baseStyles) => ({
...baseStyles,
'margin-top': '-10px',
'font-size': '13px'
fontSize: '13px'
}),
singleValue: (baseStyles, state) => ({
singleValue: (baseStyles) => ({
...baseStyles,
'margin-top': '-10px',
'font-size': '13px'
margin: '0px',
fontSize: '13px'
})
}} value={messageTemplate || ''} onChange={selectedData => {setMessageTemplate(selectedData); setMessageText(selectedData.value)}} options={[{value: '', label: ''}, ...messageTempateList.map(template => ({
value: template.message_body || '',

View File

@ -77,7 +77,7 @@ const SentMessageList = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>General</Breadcrumb.Item>
<Breadcrumb.Item href="/">General</Breadcrumb.Item>
<Breadcrumb.Item active>
Messaging
</Breadcrumb.Item>

View File

@ -41,7 +41,29 @@ const UpdateMessage = () => {
navigate(`/messages/list`);
}
const validateMessage = () => {
const errors = [];
// Required fields validation
if (!messageGroup || messageGroup === '') {
errors.push('Message Group');
}
if (!messageName || messageName.trim() === '') {
errors.push('Message Name');
}
if (errors.length > 0) {
window.alert(`Please fill in the following required fields:\n${errors.join('\n')}`);
return false;
}
return true;
};
const saveMessage = () => {
if (!validateMessage()) {
return;
}
const data = {
message_group: messageGroup,
message_title: messageTitle,
@ -57,10 +79,13 @@ const UpdateMessage = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>General</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/">General</Breadcrumb.Item>
<Breadcrumb.Item href="/messages/list">
Messaging
</Breadcrumb.Item>
<Breadcrumb.Item active>
Update Message Template
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>

View File

@ -1,76 +1,108 @@
import React, {useState, useEffect} from "react";
import { useNavigate, useParams } from "react-router-dom";
import { AuthService, ResourceService } from "../../services";
import { Archive } from "react-bootstrap-icons";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab } from "react-bootstrap";
import { RESOURCE_TYPE_OPTIONS, RESOURCE_SPECIALTY_OPTIONS } from "../../shared/constants";
const CreateResource = () => {
const navigate = useNavigate();
const urlParams = useParams();
const [name, setName] = useState('');
const [originalName, setOriginalName] = useState('');
const [branchName, setBranchName] = useState('');
const [specialty, setSpecialty] = useState('');
// Basic Information
const [name, setName] = useState(''); // Provider name
const [officeName, setOfficeName] = useState('');
const [specialty, setSpecialty] = useState('');
const [type, setType] = useState('');
const [category, setCategory] = useState('');
const [description, setDescription] = useState('');
const [color, setColor] = useState('');
const [address, setAddress] = useState('');
// Contact Information
const [phone, setPhone] = useState(''); // Office Phone Number
const [contact, setContact] = useState(''); // Secondary Phone Number
const [fax, setFax] = useState(''); // Fax Number
const [email, setEmail] = useState('');
// Address (split fields)
const [addressLine1, setAddressLine1] = useState('');
const [addressLine2, setAddressLine2] = useState('');
const [city, setCity] = useState('');
const [state, setState] = useState('');
const [zipcode, setZipcode] = useState('');
const [contact, setContact] = useState('');
const [phone, setPhone] = useState('');
const [dataObject, setDataObject] = useState(undefined);
const [note, setNote] = useState('');
const [fax, setFax] = useState('');
const [status, setStatus] = useState('active');
const [email, setEmail] = useState('');
// Additional Information
const [note, setNote] = useState('');
const redirectTo = (id) => {
const redirectTo = () => {
navigate(`/medical/resources/list`);
}
const redirectToView = (id) => {
navigate(`/medical/resources/${id}`);
navigate(`/medical/resources/${id}`);
}
const validateResource = () => {
const errors = [];
// Required fields validation
if (!name || name.trim() === '') {
errors.push('Provider');
}
if (!phone || phone.trim() === '') {
errors.push('Office Phone Number');
}
if (errors.length > 0) {
window.alert(`Please fill in the following required fields:\n${errors.join('\n')}`);
return false;
}
return true;
};
const saveResource = () => {
if (!validateResource()) {
return;
}
const combinedAddress = `${addressLine1}${addressLine2 ? ', ' + addressLine2 : ''}, ${city}, ${state} ${zipcode}`.trim();
const newResource = {
parent_id: '5eee3552b02fac3d4acfd5ea',
ext_id: '',
data: dataObject? JSON.parse(dataObject) : {},
name,
description,
note,
name_original: originalName,
name_branch: branchName,
data: {},
// Basic Information
name, // Provider
office_name: officeName,
name_original: officeName, // Legacy field
specialty,
type,
// Contact Information
phone, // Office Phone Number
contact, // Secondary Phone Number
fax, // Fax Number
email,
category,
color,
address,
state,
// Address (split fields)
address_line_1: addressLine1,
address_line_2: addressLine2,
city,
state,
zipcode,
contact,
phone,
fax,
status,
address: combinedAddress, // Legacy field
// Additional Information
note,
// System fields
status: 'active',
create_by: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name,
create_date: new Date(),
edit_by: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name,
edit_date: new Date(),
edit_history:[{ employee: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name, date: new Date() }]
}
console.log('new Resource', newResource);
edit_history: [{ employee: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name, date: new Date() }]
};
ResourceService.createNewResource(newResource).then(data => redirectToView(data?.data?.id));
};
useEffect(() => {
if (!AuthService.canAccessLegacySystem()) {
@ -80,13 +112,12 @@ const CreateResource = () => {
}
}, []);
return (
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Medical</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/medical/index">Medical</Breadcrumb.Item>
<Breadcrumb.Item href="/medical/resources/list">
Provider Information
</Breadcrumb.Item>
<Breadcrumb.Item active>
@ -94,126 +125,103 @@ const CreateResource = () => {
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>Create New Provider <button className="btn btn-link btn-sm" onClick={() => {redirectTo()}}>Back</button></h4>
<h4>Create New Provider <button className="btn btn-link btn-sm" onClick={() => {redirectTo()}}>Back</button></h4>
</div>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="providerInfo" id="providerss-tab">
<Tab eventKey="providerInfo" title="Provider Information">
<h6 className="text-primary">Basic Information</h6>
<div className="app-main-content-fields-section">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="providerInfo" id="providers-tab">
<Tab eventKey="providerInfo" title="Provider Information">
<h6 className="text-primary mt-4">Basic Information</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Doctor Name
<span className="required">*</span>
</div>
<input placeholder="e.g.,Cao Qing" type="text" value={name || ''} onChange={e => setName(e.target.value)}/>
</div>
<div className="field-label">Provider <span className="required">*</span></div>
<input placeholder="e.g., Dr. Cao Qing" type="text" value={name} onChange={e => setName(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Office Name
</div>
<input type="text" placeholder="e.g.,Silver Spring Family Medicine Clinic" value={originalName || ''} onChange={e => setOriginalName(e.target.value)}/>
</div>
<div className="field-label">Office Name</div>
<input type="text" placeholder="e.g., Silver Spring Family Medicine Clinic" value={officeName} onChange={e => setOfficeName(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Branch Name
</div>
<input type="text" placeholder="e.g.,Silver Spring" value={branchName || ''} onChange={e => setBranchName(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Specialty
</div>
<select value={specialty} onChange={e => setSpecialty(e.target.value)}>
<option value=""></option>
{
ResourceService.resourceOptionList.map((item) => <option value={item}>{item}</option>)
}
<div className="field-label">Type</div>
<select value={type} onChange={e => setType(e.target.value)}>
<option value="">Select...</option>
{RESOURCE_TYPE_OPTIONS.map((item, index) => (
<option key={index} value={item.value}>{item.label}</option>
))}
</select>
</div>
</div>
<div className="me-4">
<div className="field-label">Type
</div>
<select value={type} onChange={e => setType(e.target.value)}>
<option value="doctor">Doctor</option>
<option value="pharmacy">Pharmacy</option>
<option value="hospital">Hospital</option>
<option value="surgical center">Surgical Center</option>
<option value="government agency">Government Agency</option>
<option value="other">Other</option>
<div className="field-label">Specialty</div>
<select value={specialty} onChange={e => setSpecialty(e.target.value)}>
<option value="">Select...</option>
{RESOURCE_SPECIALTY_OPTIONS.map((item, index) => (
<option key={index} value={item.value}>{item.label}</option>
))}
</select>
</div>
</div>
</div>
<h6 className="text-primary">Contact Information</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Office Phone Number
<span className="required">*</span>
</div>
<input placeholder="e.g.,240-463-1098" type="text" value={phone || ''} onChange={e => setPhone(e.target.value)}/>
</div>
<div className="field-label">Office Phone Number <span className="required">*</span></div>
<input placeholder="e.g., 240-463-1098" type="text" value={phone} onChange={e => setPhone(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Contact
</div>
{/* <textarea value={contact || ''} onChange={e => setContact(e.target.value)}/> */}
<input type="text" placeholder="e.g.,240-463-1698" value={contact || ''} onChange={e => setContact(e.target.value)}/>
</div>
<div className="field-label">Secondary Phone Number</div>
<input type="text" placeholder="e.g., 240-463-1698" value={contact} onChange={e => setContact(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Fax Number
</div>
<input type="text" placement="e.g.,240-463-1698" value={fax || ''} onChange={e => setFax(e.target.value)}/>
</div>
<div className="field-label">Fax Number</div>
<input type="text" placeholder="e.g., 240-463-1698" value={fax} onChange={e => setFax(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Email
</div>
<input type="email" placement="e.g.,example@gmail.com" value={email || ''} onChange={e => setEmail(e.target.value)}/>
</div>
<div className="field-label">Email</div>
<input type="email" placeholder="e.g., example@gmail.com" value={email} onChange={e => setEmail(e.target.value)}/>
</div>
</div>
<h6 className="text-primary">Provider Address</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Address Line 1
<span className="required">*</span>
</div>
<input type="text" placement="e.g.,555 Cloverly Forest Dr" value={address || ''} onChange={e => setAddress(e.target.value)}/>
</div>
<div className="field-label">Address Line 1 <span className="required">*</span></div>
<input type="text" placeholder="e.g., 555 Cloverly Forest Dr" value={addressLine1} onChange={e => setAddressLine1(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">City
<span className="required">*</span>
</div>
<input type="text" placement="e.g.,Rockville" value={city || ''} onChange={e => setCity(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">State
<span className="required">*</span>
</div>
<input placement="e.g.,MD" type="text" value={state || ''} onChange={e => setState(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Zip Code
<span className="required">*</span>
</div>
<input type="text" value={zipcode || ''} onChange={e => setZipcode(e.target.value)}/>
</div>
<div className="field-label">Address Line 2</div>
<input type="text" placeholder="e.g., Suite 200" value={addressLine2} onChange={e => setAddressLine2(e.target.value)}/>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">City <span className="required">*</span></div>
<input type="text" placeholder="e.g., Rockville" value={city} onChange={e => setCity(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">State <span className="required">*</span></div>
<input type="text" placeholder="e.g., MD" value={state} onChange={e => setState(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Zip Code <span className="required">*</span></div>
<input type="text" placeholder="e.g., 20850" value={zipcode} onChange={e => setZipcode(e.target.value)}/>
</div>
</div>
<h6 className="text-primary">Additional Information</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Note
</div>
<textarea value={note || ''} onChange={e => setNote(e.target.value)}/>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Description
</div>
<input type="text" placeholder={'e.g.,Description'} value={description || ''} onChange={e => setDescription(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Color
</div>
<input type="text" placeholder={'e.g.,red'} value={color || ''} onChange={e => setColor(e.target.value)}/>
</div>
<div className="field-label">Note</div>
<textarea
placeholder="e.g., Preferred provider for cardiology referrals"
value={note}
onChange={e => setNote(e.target.value)}
rows={4}
style={{width: '400px'}}
/>
</div>
</div>
<div className="list row mb-5">
<div className="col-md-12 col-sm-12 col-xs-12">
<button className="btn btn-default btn-sm float-right" onClick={() => redirectTo()}> Cancel </button>
@ -224,16 +232,8 @@ const CreateResource = () => {
</Tabs>
</div>
</div>
{/* <div className="col-md-4 mb-4">
<div>Status:</div>
<select value={status} onChange={e => setStatus(e.target.value)}>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div> */}
</>
);
};
export default CreateResource;
export default CreateResource;

View File

@ -22,8 +22,13 @@ const ResourcesList = () => {
const [currentPage, setCurrentPage] = useState(1);
const [columns, setColumns] = useState([
{
key: 'display_name',
label: 'Name',
key: 'name',
label: 'Provider',
show: true
},
{
key: 'office_name',
label: 'Office Name',
show: true
},
{
@ -36,20 +41,20 @@ const ResourcesList = () => {
label: 'Specialty',
show: true
},
{
key: 'phone',
label: 'Office Phone Number',
show: true
},
{
key: 'email',
label: 'Email',
show: true
},
{
key: 'address',
key: 'display_address',
label: 'Address',
show: true
},
{
key: 'display_contact',
label: 'Contact',
show: true
}
]);
useEffect(() => {
@ -71,7 +76,19 @@ const ResourcesList = () => {
return a[sorting.key]?.localeCompare(b[sorting.key]);
});
const results = sorting.order === 'asc' ? sortedResources : sortedResources.reverse();
setCurrentItems(results.slice(itemOffset, endOffset)?.map(item => Object.assign({}, item, { display_name: `${item?.name || ''}${item?.name ? '-': ''}${item?.name_original || ''}${item?.name_original ? '-': ''}${item?.name_branch || ''}`}, {display_contact: item?.contact || item?.phone})));
setCurrentItems(results.slice(itemOffset, endOffset)?.map(item => {
// Format address for display
const line1 = item?.address_line_1 || item?.address || '';
const city = item?.city || '';
const state = item?.state || '';
const zipcode = item?.zipcode || '';
const display_address = [line1, city, state, zipcode].filter(Boolean).join(', ') || '-';
return Object.assign({}, item, {
office_name: item?.office_name || item?.name_original || '-',
display_address
});
}));
setPageCount(Math.ceil(((showDeletedItems ? resources : resources.filter(item => item.status === 'active') )?.filter(resource => (resource?.name?.toLowerCase().includes(keyword.toLowerCase()) || (resource?.specialty?.toLowerCase().includes(keyword.toLowerCase()) ) || (resource?.address?.toLowerCase().includes(keyword.toLowerCase()) )))).length / itemsPerPage));
}, [resources, itemOffset, keyword, itemsPerPage, showDeletedItems, sorting]);
@ -194,13 +211,14 @@ const ResourcesList = () => {
{
currentItems?.map((resource, index) => <tr key={resource?.id}>
<td className="td-checkbox"><input type="checkbox" checked={selectedItems.includes(resource.id)} onClick={()=>toggleItem(resource?.id)}/></td>
<td className="td-index">{index + 1}</td>
{columns.find(col => col.key === 'display_name')?.show && <td> {AuthService.canAccessLegacySystem() && <PencilSquare size={16} className="clickable me-2" onClick={() => goToEdit(resource?.id)}></PencilSquare>} {AuthService.canAccessLegacySystem() ? <button className="btn btn-link btn-sm" onClick={() => goToView(resource?.id)}>{resource?.display_name}</button> : resource?.display_name } </td>}
{columns.find(col => col.key === 'type')?.show && <td>{resource?.type}</td>}
{columns.find(col => col.key === 'specialty')?.show && <td>{resource?.specialty}</td>}
{columns.find(col => col.key === 'email')?.show && <td>{resource?.email}</td>}
{columns.find(col => col.key === 'address')?.show && <td>{resource?.address}</td>}
{columns.find(col => col.key === 'display_contact')?.show && <td>{resource?.display_contact}</td>}
<td className="td-index">{itemOffset + index + 1}</td>
{columns.find(col => col.key === 'name')?.show && <td> {AuthService.canAccessLegacySystem() && <PencilSquare size={16} className="clickable me-2" onClick={() => goToEdit(resource?.id)}></PencilSquare>} {AuthService.canAccessLegacySystem() ? <button className="btn btn-link btn-sm" onClick={() => goToView(resource?.id)}>{resource?.name || '-'}</button> : (resource?.name || '-') } </td>}
{columns.find(col => col.key === 'office_name')?.show && <td>{resource?.office_name || '-'}</td>}
{columns.find(col => col.key === 'type')?.show && <td>{resource?.type || '-'}</td>}
{columns.find(col => col.key === 'specialty')?.show && <td>{resource?.specialty || '-'}</td>}
{columns.find(col => col.key === 'phone')?.show && <td>{resource?.phone || '-'}</td>}
{columns.find(col => col.key === 'email')?.show && <td>{resource?.email || '-'}</td>}
{columns.find(col => col.key === 'display_address')?.show && <td>{resource?.display_address}</td>}
</tr>)
}
</tbody>
@ -249,7 +267,7 @@ const ResourcesList = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Medical</Breadcrumb.Item>
<Breadcrumb.Item href="/medical/index">Medical</Breadcrumb.Item>
<Breadcrumb.Item active>
Provider Information
</Breadcrumb.Item>
@ -260,15 +278,15 @@ const ResourcesList = () => {
</h4>
</div>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-container list-page">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="activeProviders" id="provider-tab" onSelect={(k) => showArchive(k)}>
<Tab eventKey="activeProviders" title="Active Providers">
{table}
</Tab>
<Tab eventKey="archivedProviders" title="Archived Providers">
{/* <Tab eventKey="archivedProviders" title="Archived Providers">
{table}
</Tab>
</Tab> */}
</Tabs>
<div className="list-func-panel">
<input className="me-2 with-search-icon" type="text" placeholder="Search" value={keyword} onChange={(e) => setKeyword(e.currentTarget.value)} />

View File

@ -2,75 +2,129 @@ import React, {useState, useEffect} from "react";
import { useNavigate, useParams } from "react-router-dom";
import { AuthService, ResourceService } from "../../services";
import { Archive } from "react-bootstrap-icons";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab } from "react-bootstrap";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab, Modal, Button } from "react-bootstrap";
import { RESOURCE_TYPE_OPTIONS, RESOURCE_SPECIALTY_OPTIONS } from "../../shared/constants";
const UpdateResource = () => {
const navigate = useNavigate();
const urlParams = useParams();
const [currentResource, setCurrentResource] = useState(undefined);
const [name, setName] = useState('');
const [originalName, setOriginalName] = useState('');
const [specialty, setSpecialty] = useState('');
const [currentResource, setCurrentResource] = useState(undefined);
// Basic Information
const [name, setName] = useState(''); // Provider name
const [officeName, setOfficeName] = useState('');
const [specialty, setSpecialty] = useState('');
const [type, setType] = useState('');
const [category, setCategory] = useState('');
const [description, setDescription] = useState('');
const [color, setColor] = useState('');
const [address, setAddress] = useState('');
// Contact Information
const [phone, setPhone] = useState(''); // Office Phone Number
const [contact, setContact] = useState(''); // Secondary Phone Number
const [fax, setFax] = useState(''); // Fax Number
const [email, setEmail] = useState('');
// Address (split fields)
const [addressLine1, setAddressLine1] = useState('');
const [addressLine2, setAddressLine2] = useState('');
const [city, setCity] = useState('');
const [state, setState] = useState('');
const [zipcode, setZipcode] = useState('');
const [contact, setContact] = useState('');
const [phone, setPhone] = useState('');
const [dataObject, setDataObject] = useState(undefined);
const [note, setNote] = useState('');
const [fax, setFax] = useState('');
const [status, setStatus] = useState('');
const [branchName, setBranchName] = useState('');
const [email, setEmail] = useState('');
const params = new URLSearchParams(window.location.search);
// Additional Information
const [note, setNote] = useState('');
// Modal
const [showDeleteModal, setShowDeleteModal] = useState(false);
const redirectTo = (id) => {
const redirectTo = () => {
navigate(`/medical/resources/list`);
}
const redirectToView = () => {
navigate(`/medical/resources/${urlParams.id}`);
navigate(`/medical/resources/${urlParams.id}`);
}
const validateResource = () => {
const errors = [];
// Required fields validation
if (!name || name.trim() === '') {
errors.push('Provider');
}
if (!phone || phone.trim() === '') {
errors.push('Office Phone Number');
}
if (errors.length > 0) {
window.alert(`Please fill in the following required fields:\n${errors.join('\n')}`);
return false;
}
return true;
};
const saveResource = () => {
const newResource = {
if (!validateResource()) {
return;
}
const combinedAddress = `${addressLine1}${addressLine2 ? ', ' + addressLine2 : ''}, ${city}, ${state} ${zipcode}`.trim();
const updatedResource = {
...currentResource,
data: JSON.parse(dataObject),
name,
description,
note,
name_original: originalName,
name_branch: branchName,
data: currentResource?.data || {},
// Basic Information
name, // Provider
office_name: officeName,
name_original: officeName, // Legacy field
specialty,
type,
category,
color,
address,
state,
city,
zipcode,
contact,
phone,
// Contact Information
phone, // Office Phone Number
contact, // Secondary Phone Number
fax, // Fax Number
email,
fax,
status,
// Address (split fields)
address_line_1: addressLine1,
address_line_2: addressLine2,
city,
state,
zipcode,
address: combinedAddress, // Legacy field
// Additional Information
note,
// System fields
edit_by: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name,
edit_date: new Date(),
edit_history: currentResource?.edit_history? [...currentResource.edit_history, { employee: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name, date: new Date() }] : [{ employee: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name, date: new Date() }]
}
console.log('new Resource', newResource);
edit_history: currentResource?.edit_history
? [...currentResource.edit_history, { employee: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name, date: new Date() }]
: [{ employee: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name, date: new Date() }]
};
ResourceService.updateResource(urlParams.id, newResource).then(data => redirectToView());
ResourceService.updateResource(urlParams.id, updatedResource).then(() => redirectToView());
};
const triggerShowDeleteModal = () => {
setShowDeleteModal(true);
}
const closeDeleteModal = () => {
setShowDeleteModal(false);
}
const deactivateResource = () => {
ResourceService.disableResource(urlParams.id, {
status: 'inactive',
edit_by: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name,
edit_date: new Date()
}).then(() => {
setShowDeleteModal(false);
redirectTo();
});
}
useEffect(() => {
if (!AuthService.canAccessLegacySystem()) {
@ -80,39 +134,41 @@ const UpdateResource = () => {
}
ResourceService.getResource(urlParams.id).then(resourceData => {
setCurrentResource(resourceData.data);
})
});
}, []);
useEffect(() => {
if (currentResource) {
setName(currentResource?.name);
setOriginalName(currentResource?.name_original);
setBranchName(currentResource?.name_branch);
setSpecialty(currentResource?.specialty);
setType(currentResource?.type);
setCategory(currentResource?.category);
setDescription(currentResource?.description);
setColor(currentResource?.color);
setAddress(currentResource?.address);
setState(currentResource?.state);
setCity(currentResource?.city);
setZipcode(currentResource?.zipcode);
setContact(currentResource?.contact);
setPhone(currentResource?.phone);
setFax(currentResource?.fax);
setNote(currentResource?.note);
setStatus(currentResource?.status);
setDataObject(JSON.stringify(currentResource?.data || {}));
// Basic Information
setName(currentResource?.name || '');
setOfficeName(currentResource?.office_name || currentResource?.name_original || '');
setSpecialty(currentResource?.specialty || '');
setType(currentResource?.type || '');
// Contact Information
setPhone(currentResource?.phone || '');
setContact(currentResource?.contact || '');
setFax(currentResource?.fax || '');
setEmail(currentResource?.email || '');
// Address - try new fields first, then fall back to legacy
setAddressLine1(currentResource?.address_line_1 || currentResource?.address || '');
setAddressLine2(currentResource?.address_line_2 || '');
setCity(currentResource?.city || '');
setState(currentResource?.state || '');
setZipcode(currentResource?.zipcode || '');
// Additional Information - merge description into note if note is empty
setNote(currentResource?.note || currentResource?.description || '');
}
}, [currentResource]);
return (
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Medical</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/medical/index">Medical</Breadcrumb.Item>
<Breadcrumb.Item href="/medical/resources/list">
Provider Information
</Breadcrumb.Item>
<Breadcrumb.Item active>
@ -125,144 +181,130 @@ const UpdateResource = () => {
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="providerInfo" id="providerss-tab">
<Tabs defaultActiveKey="providerInfo" id="providers-tab">
<Tab eventKey="providerInfo" title="Provider Information">
<h6 className="text-primary">Basic Information</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Doctor Name
<span className="required">*</span>
</div>
<input placeholder="e.g.,Cao Qing" type="text" value={name || ''} onChange={e => setName(e.target.value)}/>
<div className="field-label">Provider <span className="required">*</span></div>
<input placeholder="e.g., Dr. Cao Qing" type="text" value={name} onChange={e => setName(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Office Name
</div>
<input type="text" placeholder="e.g.,Silver Spring Family Medicine Clinic" value={originalName || ''} onChange={e => setOriginalName(e.target.value)}/>
<div className="field-label">Office Name</div>
<input type="text" placeholder="e.g., Silver Spring Family Medicine Clinic" value={officeName} onChange={e => setOfficeName(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Branch Name
</div>
<input type="text" placeholder="e.g.,Silver Spring" value={branchName || ''} onChange={e => setBranchName(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Specialty
</div>
<select value={specialty} onChange={e => setSpecialty(e.target.value)}>
<option value=""></option>
{
ResourceService.resourceOptionList.map((item) => <option value={item}>{item}</option>)
}
<div className="field-label">Type</div>
<select value={type} onChange={e => setType(e.target.value)}>
<option value="">Select...</option>
{RESOURCE_TYPE_OPTIONS.map((item, index) => (
<option key={index} value={item.value}>{item.label}</option>
))}
</select>
</div>
<div className="me-4">
<div className="field-label">Type
</div>
<select value={type} onChange={e => setType(e.target.value)}>
<option value="doctor">Doctor</option>
<option value="pharmacy">Pharmacy</option>
<option value="hospital">Hospital</option>
<option value="surgical center">Surgical Center</option>
<option value="government agency">Government Agency</option>
<option value="other">Other</option>
<div className="field-label">Specialty</div>
<select value={specialty} onChange={e => setSpecialty(e.target.value)}>
<option value="">Select...</option>
{RESOURCE_SPECIALTY_OPTIONS.map((item, index) => (
<option key={index} value={item.value}>{item.label}</option>
))}
</select>
</div>
</div>
<h6 className="text-primary">Contact Information</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Office Phone Number
<span className="required">*</span>
</div>
<input placeholder="e.g.,240-463-1098" type="text" value={phone || ''} onChange={e => setPhone(e.target.value)}/>
<div className="field-label">Office Phone Number <span className="required">*</span></div>
<input placeholder="e.g., 240-463-1098" type="text" value={phone} onChange={e => setPhone(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Contact
</div>
{/* <textarea value={contact || ''} onChange={e => setContact(e.target.value)}/> */}
<input type="text" placeholder="e.g.,240-463-1698" value={contact || ''} onChange={e => setContact(e.target.value)}/>
<div className="field-label">Secondary Phone Number</div>
<input type="text" placeholder="e.g., 240-463-1698" value={contact} onChange={e => setContact(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Fax Number
</div>
<input type="text" placement="e.g.,240-463-1698" value={fax || ''} onChange={e => setFax(e.target.value)}/>
<div className="field-label">Fax Number</div>
<input type="text" placeholder="e.g., 240-463-1698" value={fax} onChange={e => setFax(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Email
</div>
<input type="email" placement="e.g.,example@gmail.com" value={email || ''} onChange={e => setEmail(e.target.value)}/>
<div className="field-label">Email</div>
<input type="email" placeholder="e.g., example@gmail.com" value={email} onChange={e => setEmail(e.target.value)}/>
</div>
</div>
<h6 className="text-primary">Provider Address</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Address Line 1
<span className="required">*</span>
</div>
<input type="text" placement="e.g.,555 Cloverly Forest Dr" value={address || ''} onChange={e => setAddress(e.target.value)}/>
<div className="field-label">Address Line 1 <span className="required">*</span></div>
<input type="text" placeholder="e.g., 555 Cloverly Forest Dr" value={addressLine1} onChange={e => setAddressLine1(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">City
<span className="required">*</span>
</div>
<input type="text" placement="e.g.,Rockville" value={city || ''} onChange={e => setCity(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">State
<span className="required">*</span>
</div>
<input placement="e.g.,MD" type="text" value={state || ''} onChange={e => setState(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Zip Code
<span className="required">*</span>
</div>
<input type="text" value={zipcode || ''} onChange={e => setZipcode(e.target.value)}/>
<div className="field-label">Address Line 2</div>
<input type="text" placeholder="e.g., Suite 200" value={addressLine2} onChange={e => setAddressLine2(e.target.value)}/>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">City <span className="required">*</span></div>
<input type="text" placeholder="e.g., Rockville" value={city} onChange={e => setCity(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">State <span className="required">*</span></div>
<input type="text" placeholder="e.g., MD" value={state} onChange={e => setState(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Zip Code <span className="required">*</span></div>
<input type="text" placeholder="e.g., 20850" value={zipcode} onChange={e => setZipcode(e.target.value)}/>
</div>
</div>
<h6 className="text-primary">Additional Information</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Note
</div>
<textarea value={note || ''} onChange={e => setNote(e.target.value)}/>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Description
</div>
<input type="text" placeholder={'e.g.,Description'} value={description || ''} onChange={e => setDescription(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Color
</div>
<input type="text" placeholder={'e.g.,red'} value={color || ''} onChange={e => setColor(e.target.value)}/>
<div className="field-label">Note</div>
<textarea
placeholder="e.g., Preferred provider for cardiology referrals"
value={note}
onChange={e => setNote(e.target.value)}
rows={4}
style={{width: '400px'}}
/>
</div>
</div>
<div className="list row mb-5">
<div className="col-md-12 col-sm-12 col-xs-12">
<button className="btn btn-default btn-sm float-right" onClick={() => redirectTo()}> Cancel </button>
<button className="btn btn-danger btn-sm me-2 mb-2" onClick={() => triggerShowDeleteModal()}> Delete </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => saveResource()}> Save </button>
</div>
</div>
</Tab>
</Tabs>
<div className="list-func-panel">
<button className="btn btn-primary" onClick={() => { ResourceService.disableResource(urlParams?.id, {status: 'inactive'})}}><Archive size={16} className="me-2"></Archive>Archive</button>
<button className="btn btn-primary" onClick={() => deactivateResource()}><Archive size={16} className="me-2"></Archive>Archive</button>
</div>
</div>
</div>
{/* <div className="col-md-4 mb-4">
<div>Status:</div>
<select value={status} onChange={e => setStatus(e.target.value)}>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div> */}
</>
<Modal show={showDeleteModal} onHide={() => closeDeleteModal()}>
<Modal.Header closeButton>
<Modal.Title>Delete Provider</Modal.Title>
</Modal.Header>
<Modal.Body>
<div>Are you sure you want to delete this provider?</div>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={() => closeDeleteModal()}>
No
</Button>
<Button variant="primary" onClick={() => deactivateResource()}>
Yes
</Button>
</Modal.Footer>
</Modal>
</>
);
};
export default UpdateResource;
export default UpdateResource;

View File

@ -1,33 +1,51 @@
import React, {useState, useEffect} from "react";
// import { useDispatch } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
// import { customerSlice } from "./../../store";
import { AuthService, ResourceService } from "../../services";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab } from "react-bootstrap";
import { Download, Pencil, Archive } from "react-bootstrap-icons";
import { RESOURCE_TYPE_TEXT } from "../../shared/constants";
const ViewResource = () => {
const navigate = useNavigate();
const urlParams = useParams();
const [currentResource, setCurrentResource] = useState(undefined);
const params = new URLSearchParams(window.location.search);
const [currentResource, setCurrentResource] = useState(undefined);
const redirectTo = () => {
navigate(`/medical/resources/list`);
}
const goToEdit = (id) => {
navigate(`/medical/resources/edit/${id}`)
navigate(`/medical/resources/edit/${id}`);
}
const deactivateResource = (id) => {
const data = {
status: 'inactive'
status: 'inactive',
edit_by: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name,
edit_date: new Date()
};
ResourceService.disableResource(id, data).then(() => {
redirectTo();
})
});
}
// Helper function to format address
const formatAddress = () => {
const line1 = currentResource?.address_line_1 || currentResource?.address || '';
const line2 = currentResource?.address_line_2 || '';
const city = currentResource?.city || '';
const state = currentResource?.state || '';
const zipcode = currentResource?.zipcode || '';
if (!line1 && !city && !state && !zipcode) return '-';
let address = line1;
if (line2) address += `, ${line2}`;
if (city) address += `, ${city}`;
if (state) address += `, ${state}`;
if (zipcode) address += ` ${zipcode}`;
return address || '-';
}
useEffect(() => {
@ -38,15 +56,15 @@ const ViewResource = () => {
}
ResourceService.getResource(urlParams.id).then(resourceData => {
setCurrentResource(resourceData.data);
})
});
}, []);
return (
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Medical</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/medical/index">Medical</Breadcrumb.Item>
<Breadcrumb.Item href="/medical/resources/list">
Provider Information
</Breadcrumb.Item>
<Breadcrumb.Item active>
@ -54,83 +72,89 @@ const ViewResource = () => {
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>View Provider Information <button className="btn btn-link btn-sm" onClick={() => {redirectTo()}}>Back</button></h4>
<h4>View Provider Information <button className="btn btn-link btn-sm" onClick={() => {redirectTo()}}>Back</button></h4>
</div>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="providerInfo" id="providerss-tab">
<Tabs defaultActiveKey="providerInfo" id="providers-tab">
<Tab eventKey="providerInfo" title="Provider Information">
<h6 className="text-primary">Basic Information</h6>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Name</div>
<div className="field-value">{currentResource?.name}</div>
<div className="field-label">Provider</div>
<div className="field-value">{currentResource?.name || '-'}</div>
</div>
<div className="field-body">
<div className="field-label">Office Name</div>
<div className="field-value">{currentResource?.name_original}</div>
</div>
<div className="field-body">
<div className="field-label">Branch Name</div>
<div className="field-value">{currentResource?.name_branch}</div>
<div className="field-value">{currentResource?.office_name || currentResource?.name_original || '-'}</div>
</div>
<div className="field-body">
<div className="field-label">Specialty</div>
<div className="field-value">{currentResource?.specialty}</div>
<div className="field-value">{currentResource?.specialty || '-'}</div>
</div>
<div className="field-body">
<div className="field-label">Type</div>
<div className="field-value">{currentResource?.type}</div>
<div className="field-value">{RESOURCE_TYPE_TEXT[currentResource?.type] || currentResource?.type || '-'}</div>
</div>
</div>
<h6 className="text-primary">Contact Information</h6>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Office Phone Number</div>
<div className="field-value">{currentResource?.phone}</div>
<div className="field-value">{currentResource?.phone || '-'}</div>
</div>
<div className="field-body">
<div className="field-label">Contact</div>
<div className="field-value">{currentResource?.contact}</div>
<div className="field-label">Secondary Phone Number</div>
<div className="field-value">{currentResource?.contact || '-'}</div>
</div>
<div className="field-body">
<div className="field-label">Fax Number</div>
<div className="field-value">{currentResource?.fax}</div>
<div className="field-value">{currentResource?.fax || '-'}</div>
</div>
<div className="field-body">
<div className="field-label">Email</div>
<div className="field-value">{currentResource?.email || '-'}</div>
</div>
</div>
<h6 className="text-primary">Provider Address</h6>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Address</div>
<div className="field-value">{`${currentResource?.address}, ${currentResource?.city}, ${currentResource?.state}, ${currentResource?.zipcode}`}</div>
<div className="field-label">Address Line 1</div>
<div className="field-value">{currentResource?.address_line_1 || currentResource?.address || '-'}</div>
</div>
<div className="field-body">
<div className="field-label">Address Line 2</div>
<div className="field-value">{currentResource?.address_line_2 || '-'}</div>
</div>
<div className="field-body">
<div className="field-label">City</div>
<div className="field-value">{currentResource?.city || '-'}</div>
</div>
<div className="field-body">
<div className="field-label">State</div>
<div className="field-value">{currentResource?.state || '-'}</div>
</div>
<div className="field-body">
<div className="field-label">Zip Code</div>
<div className="field-value">{currentResource?.zipcode || '-'}</div>
</div>
</div>
<h6 className="text-primary">Additional Information</h6>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Note</div>
<div className="field-value">{currentResource?.note}</div>
</div>
<div className="field-body">
<div className="field-label">Description</div>
<div className="field-value">{currentResource?.description}</div>
</div>
<div className="field-body">
<div className="field-label">Color</div>
<div className="field-value">{currentResource?.color}</div>
</div>
<div className="field-body">
<div className="field-label">Status</div>
<div className="field-value">{currentResource?.status}</div>
<div className="field-value">{currentResource?.note || currentResource?.description || '-'}</div>
</div>
</div>
</Tab>
</Tabs>
<div className="list-func-panel">
<button className="btn btn-primary me-2" onClick={() => goToEdit(currentResource?.id)}><Pencil size={16} className="me-2"></Pencil>Edit</button>
<button className="btn btn-primary me-2" onClick={() => deactivateResource()}><Archive size={16} className="me-2"></Archive>Archive</button>
{/* <button className="btn btn-primary"><Download size={16} className="me-2"></Download>Download</button> */}
<button className="btn btn-primary me-2" onClick={() => deactivateResource(currentResource?.id)}><Archive size={16} className="me-2"></Archive>Archive</button>
</div>
</div>
</div>
@ -138,4 +162,4 @@ const ViewResource = () => {
);
};
export default ViewResource;
export default ViewResource;

View File

@ -1,6 +1,6 @@
import React from 'react';
const CircularTable = ({tableNumber, guests = []}) => {
const CircularTable = ({tableNumber, guests = [], inCenterCustomerIds = [], onSeatClick}) => {
const getPositions = () => {
const positions = [];
const seatCount = 8;
@ -18,6 +18,12 @@ const CircularTable = ({tableNumber, guests = []}) => {
return positions;
}
// Check if a customer is in-center (checked in)
const isCustomerInCenter = (customerId) => {
if (!customerId) return true; // Empty seats are not grayed out
return inCenterCustomerIds.includes(customerId);
}
const seatPositions = getPositions();
// const defaultNames = ['Guest 1', 'Guest 2', 'Guest 3', 'Guest 4', 'Guest 5', 'Guest 6', 'Guest 7', 'Guest 8' ];
const defaultSeatNumber = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
@ -32,10 +38,31 @@ const CircularTable = ({tableNumber, guests = []}) => {
<div className="table-number">{tableNumber}</div>
{seatPositions.map((pos, index) => {
return (<div className="seat-container" key={index} style={{transform: `translate(${pos.x}px, ${pos.y}px)`}}>
<div className="seat-circle" style={guests[index]?.label?.label_color && { background: guests[index]?.label?.label_color}}>{defaultSeatNumber[index]}</div>
<div className="guest-name">{guests[index]?.customerName}</div>
</div>)
const guest = guests[index];
const isInCenter = isCustomerInCenter(guest?.customerId);
const hasCustomer = guest?.customerId && guest?.customerName;
// Determine seat style:
// - Apply label color if it exists
// - Gray out if customer is assigned but not in-center (and no label color)
const seatStyle = {
...(guest?.label?.label_color ? { background: guest.label.label_color } : {}),
...(!isInCenter && hasCustomer && !guest?.label?.label_color ? { background: '#cccccc', opacity: 0.6 } : {}),
...(!isInCenter && hasCustomer ? { opacity: 0.6 } : {})
};
return (<div className="seat-container" key={index} style={{transform: `translate(${pos.x}px, ${pos.y}px)`}}>
<div
className={`seat-circle ${hasCustomer ? 'has-customer' : ''}`}
style={seatStyle}
onClick={() => hasCustomer && onSeatClick && onSeatClick(guest)}
>
{defaultSeatNumber[index]}
</div>
<div className="guest-name" style={!isInCenter && hasCustomer ? { color: '#999', fontSize: '8px' } : {}}>
{guest?.customerName || ''}
</div>
</div>)
})}
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -29,8 +29,10 @@ const BreakfastSection = ({transRoutes, breakfastRecords, sectionName, confimHas
}
useEffect(() => {
const routeCustomers = TransRoutesService.getAllInCenterCustomersFromRoutes(transRoutes, breakfastRecords);
setCustomers(routeCustomers);
if (transRoutes && transRoutes.length > 0) {
const routeCustomers = TransRoutesService.getAllInCenterCustomersFromRoutes(transRoutes, breakfastRecords || []);
setCustomers(routeCustomers);
}
}, [breakfastRecords, transRoutes]);
@ -51,20 +53,20 @@ const BreakfastSection = ({transRoutes, breakfastRecords, sectionName, confimHas
<th>Change Breakfast Status</th>
</tr>
</thead>
<tbody>
{
breakfastRecords?.length >0 && customers?.map((customer) => (
<tr className={customer?.has_breakfast ? 'light-green' : 'red'}>
<td>{customer?.customer_name}</td>
<td>{customer?.has_breakfast ? 'Yes': 'No'}</td>
<td>
{!customer?.has_breakfast && <button className="btn btn-link btn-sm" onClick={() => confimHasBreakfast(customer)}>Confirm Customer Has breakfast</button>}
{customer?.has_breakfast && <button className="btn btn-link btn-sm" onClick={() => removeBreakfastRecord(customer?.customer_id)}>Mark Customer NOT have breakfast</button>}
</td>
</tr>
))
}
</tbody>
<tbody>
{
customers?.map((customer) => (
<tr key={customer?.customer_id} className={customer?.has_breakfast ? 'light-green' : 'red'}>
<td>{customer?.customer_name}</td>
<td>{customer?.has_breakfast ? 'Yes': 'No'}</td>
<td>
{!customer?.has_breakfast && <button className="btn btn-link btn-sm" onClick={() => confimHasBreakfast(customer)}>Confirm Customer Has breakfast</button>}
{customer?.has_breakfast && <button className="btn btn-link btn-sm" onClick={() => removeBreakfastRecord(customer?.customer_id)}>Mark Customer NOT have breakfast</button>}
</td>
</tr>
))
}
</tbody>
</table>
</div>
</div>

View File

@ -79,13 +79,36 @@ const CreateRoute = () => {
navigate(`/trans-routes/templates${params.get('type')?`?type=${params.get('type')}`: ''}${params.get('date')? `&date=${params.get('date')}` : ''}`);
}
const validateRoute = () => {
const errors = [];
// Required fields validation
if (!routeName || routeName.trim() === '') {
errors.push('Route Name');
}
if (!newRouteType || newRouteType === '') {
errors.push('Route Type');
}
if (!newDriver || newDriver === '') {
errors.push('Driver');
}
if (!newVehicle || newVehicle === '') {
errors.push('Vehicle');
}
if (errors.length > 0) {
window.alert(`Please fill in the following required fields:\n${errors.join('\n')}`);
return false;
}
return true;
};
const saveRoute = () => {
if (!disableSave) {
if (!validateRoute()) {
return;
}
setDisableSave(true);
if (!routeName || routeName === '') { setErrorMessage('Route Name is Required'); return;}
if (!newRouteType || newRouteType === '') {setErrorMessage('Route Type is Required'); return;}
if (!newDriver || newDriver === '') {setErrorMessage('Driver is Required'); return;}
if (!newVehicle || newVehicle === '') {setErrorMessage('Vehicle is Required'); return;}
const now = new Date();
let scheduleDate = ((now.getMonth() > 8) ? (now.getMonth() + 1) : ('0' + (now.getMonth() + 1))) + '/' + ((now.getDate() > 9) ? now.getDate() : ('0' + now.getDate())) + '/' + now.getFullYear();
if (params.get('date') === 'tomorrow') {
@ -175,10 +198,13 @@ const CreateRoute = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Transportation</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/">Transportation</Breadcrumb.Item>
<Breadcrumb.Item href="/trans-routes/dashboard">
Transportation Routes
</Breadcrumb.Item>
<Breadcrumb.Item active>
Create New Route
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>
@ -187,7 +213,7 @@ const CreateRoute = () => {
</div>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-container form-page">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="routeOverview" id="route-view-tab">
<Tab eventKey="routeOverview" title="Route Information">

View File

@ -29,8 +29,10 @@ const LunchSection = ({transRoutes, lunchRecords, sectionName, confirmHasLunch,
}
useEffect(() => {
const routeCustomers = TransRoutesService.getAllInCenterCustomersFromRoutesForLunch(transRoutes, lunchRecords);
setCustomers(routeCustomers);
if (transRoutes && transRoutes.length > 0) {
const routeCustomers = TransRoutesService.getAllInCenterCustomersFromRoutesForLunch(transRoutes, lunchRecords || []);
setCustomers(routeCustomers);
}
}, [lunchRecords, transRoutes]);
@ -51,20 +53,20 @@ const LunchSection = ({transRoutes, lunchRecords, sectionName, confirmHasLunch,
<th>Change Lunch Status</th>
</tr>
</thead>
<tbody>
{
lunchRecords?.length >0 && customers?.map((customer) => (
<tr className={customer?.has_lunch ? 'light-green' : 'red'}>
<td>{customer?.customer_name}</td>
<td>{customer?.has_lunch ? 'Yes': 'No'}</td>
<td>
{!customer?.has_lunch && <button className="btn btn-link btn-sm" onClick={() => confirmHasLunch(customer)}>Confirm Customer Has Lunch</button>}
{customer?.has_lunch && <button className="btn btn-link btn-sm" onClick={() => removeLunchRecord(customer?.customer_id)}>Mark Customer NOT have lunch</button>}
</td>
</tr>
))
}
</tbody>
<tbody>
{
customers?.map((customer) => (
<tr key={customer?.customer_id} className={customer?.has_lunch ? 'light-green' : 'red'}>
<td>{customer?.customer_name}</td>
<td>{customer?.has_lunch ? 'Yes': 'No'}</td>
<td>
{!customer?.has_lunch && <button className="btn btn-link btn-sm" onClick={() => confirmHasLunch(customer)}>Confirm Customer Has Lunch</button>}
{customer?.has_lunch && <button className="btn btn-link btn-sm" onClick={() => removeLunchRecord(customer?.customer_id)}>Mark Customer NOT have lunch</button>}
</td>
</tr>
))
}
</tbody>
</table>
</div>
</div>

View File

@ -494,28 +494,96 @@ const PersonnelInfoTable = ({transRoutes, showCompletedInfo,
const saveBulkUpdate = () => {
const routeId = transRoutes[0]?.id;
// Debug logging
console.log('=== saveBulkUpdate Debug ===');
console.log('bulkEnterCenterTime:', bulkEnterCenterTime);
console.log('bulkEnterCenterTime type:', typeof bulkEnterCenterTime);
console.log('bulkEnterCenterTime value:', JSON.stringify(bulkEnterCenterTime));
console.log('bulkLeaveCenterTime:', bulkLeaveCenterTime);
console.log('bulkLeaveCenterTime type:', typeof bulkLeaveCenterTime);
console.log('bulkLeaveCenterTime value:', JSON.stringify(bulkLeaveCenterTime));
if (routeId) {
let requestBody = transRoutes.find((route) => route.id === routeId);
const dateStr = requestBody?.schedule_date || '';
console.log('dateStr:', dateStr);
console.log('Number of customers to update:', requestBody.route_customer_list.length);
console.log('Original customer list IDs:', requestBody.route_customer_list.map(c => ({ id: c.customer_id, name: c.customer_name, pickup_status: c.customer_pickup_status })));
const updatedCustomerList = requestBody.route_customer_list.map((item) => {
// Skip customers who are Unscheduled Absent or Scheduled Absent
if (item.customer_pickup_status === PICKUP_STATUS.UNSCHEDULE_ABSENT ||
item.customer_pickup_status === PICKUP_STATUS.SCHEDULE_ABSENT) {
// Only skip if status is explicitly set to one of these values (not undefined)
if (item.customer_pickup_status &&
(item.customer_pickup_status === PICKUP_STATUS.UNSCHEDULE_ABSENT ||
item.customer_pickup_status === PICKUP_STATUS.SCHEDULE_ABSENT)) {
console.log(`Skipping customer ${item.customer_id} (${item.customer_name}) - status: ${item.customer_pickup_status}`);
return item;
}
let updatedItem = { ...item };
if (bulkEnterCenterTime && bulkEnterCenterTime !== '') {
updatedItem.customer_enter_center_time = combineDateAndTime(dateStr, bulkEnterCenterTime);
updatedItem.customer_route_status = PERSONAL_ROUTE_STATUS.IN_CENTER;
// Debug logging for each customer
console.log(`Processing customer ${item.customer_id} (${item.customer_name})`);
// Check if bulkEnterCenterTime is a valid time string (HH:mm format)
console.log('Checking bulkEnterCenterTime:', {
value: bulkEnterCenterTime,
type: typeof bulkEnterCenterTime,
isTruthy: !!bulkEnterCenterTime,
isString: typeof bulkEnterCenterTime === 'string',
trimmed: typeof bulkEnterCenterTime === 'string' ? bulkEnterCenterTime.trim() : 'N/A',
trimmedLength: typeof bulkEnterCenterTime === 'string' ? bulkEnterCenterTime.trim().length : 0
});
if (bulkEnterCenterTime && typeof bulkEnterCenterTime === 'string' && bulkEnterCenterTime.trim() !== '') {
try {
const combinedTime = combineDateAndTime(dateStr, bulkEnterCenterTime);
const dateValue = combinedTime.toDate();
console.log('Combined enter center time (moment):', combinedTime);
console.log('Combined enter center time (Date):', dateValue);
updatedItem.customer_enter_center_time = dateValue;
// Don't automatically update status when setting time
console.log(`Set customer_enter_center_time for ${item.customer_name} to:`, updatedItem.customer_enter_center_time);
} catch (e) {
console.error('Error combining date and enter center time:', e);
}
} else {
console.log('Skipping bulkEnterCenterTime - condition not met');
}
if (bulkLeaveCenterTime && bulkLeaveCenterTime !== '') {
updatedItem.customer_leave_center_time = combineDateAndTime(dateStr, bulkLeaveCenterTime);
updatedItem.customer_route_status = PERSONAL_ROUTE_STATUS.LEFT_CENTER;
// Check if bulkLeaveCenterTime is a valid time string (HH:mm format)
console.log('Checking bulkLeaveCenterTime:', {
value: bulkLeaveCenterTime,
type: typeof bulkLeaveCenterTime,
isTruthy: !!bulkLeaveCenterTime,
isString: typeof bulkLeaveCenterTime === 'string',
trimmed: typeof bulkLeaveCenterTime === 'string' ? bulkLeaveCenterTime.trim() : 'N/A',
trimmedLength: typeof bulkLeaveCenterTime === 'string' ? bulkLeaveCenterTime.trim().length : 0
});
if (bulkLeaveCenterTime && typeof bulkLeaveCenterTime === 'string' && bulkLeaveCenterTime.trim() !== '') {
try {
const combinedTime = combineDateAndTime(dateStr, bulkLeaveCenterTime);
const dateValue = combinedTime.toDate();
console.log('Combined leave center time (moment):', combinedTime);
console.log('Combined leave center time (Date):', dateValue);
updatedItem.customer_leave_center_time = dateValue;
// Don't automatically update status when setting time
console.log(`Set customer_leave_center_time for ${item.customer_name} to:`, updatedItem.customer_leave_center_time);
} catch (e) {
console.error('Error combining date and leave center time:', e);
}
} else {
console.log('Skipping bulkLeaveCenterTime - condition not met');
}
console.log(`Final updatedItem for ${item.customer_name}:`, {
customer_id: updatedItem.customer_id,
customer_enter_center_time: updatedItem.customer_enter_center_time,
customer_leave_center_time: updatedItem.customer_leave_center_time
});
if (bulkCustomerRouteStatus && bulkCustomerRouteStatus !== '') {
updatedItem.customer_route_status = bulkCustomerRouteStatus;
@ -529,6 +597,19 @@ const PersonnelInfoTable = ({transRoutes, showCompletedInfo,
});
requestBody = Object.assign({}, requestBody, {route_customer_list: updatedCustomerList, updatedAt: new Date(), updatedBy: 'admin'});
// Debug: Log all customers from updated list
console.log('=== All Updated Customers ===');
updatedCustomerList.forEach((customer, index) => {
console.log(`Customer ${index + 1}:`, {
customer_id: customer.customer_id,
customer_name: customer.customer_name,
pickup_status: customer.customer_pickup_status,
customer_enter_center_time: customer.customer_enter_center_time,
customer_leave_center_time: customer.customer_leave_center_time
});
});
let finalParams = { id: routeId, data: requestBody };
if (scheduleDate) {
finalParams = Object.assign({}, finalParams, {dateText: moment(scheduleDate).format('MM/DD/YYYY'), fromSchedule: true})
@ -536,6 +617,28 @@ const PersonnelInfoTable = ({transRoutes, showCompletedInfo,
if (dateStr !== '' && dateStr !== moment().format('MM/DD/YYYY')) {
finalParams = Object.assign({}, finalParams, {dateText: dateStr})
}
// Log final payload before dispatch, especially for Wang,Huanran
const wangCustomer = finalParams.data.route_customer_list?.find(c =>
c.customer_id === '63657acff745ffd72affb8d8' || c.customer_name?.includes('Wang,Huanran')
);
console.log('Wang,Huanran customer in final payload:', wangCustomer);
console.log('Final params being dispatched:', {
id: finalParams.id,
data_keys: Object.keys(finalParams.data),
customer_count: finalParams.data.route_customer_list?.length
});
// Stringify to see what would actually be sent (Date serialization)
try {
const stringified = JSON.stringify(finalParams.data.route_customer_list?.slice(0, 2));
console.log('Sample serialized customer list (first 2):', stringified);
} catch (e) {
console.error('Error stringifying:', e);
}
console.log('=== End saveBulkUpdate Debug ===');
dispatch(updateRoute(finalParams));
} else {
window.alert('Fail to update Route: no route Id found.')
@ -625,8 +728,8 @@ const PersonnelInfoTable = ({transRoutes, showCompletedInfo,
<th>Type</th>
{!showCompletedInfo &&<th>Route Type</th>}
<th>Pick Up Time</th>
{!showCompletedInfo && (<th>Enter Center Time</th>)}
{!showCompletedInfo && (<th>Leave Center Time</th>)}
<th>Enter Center Time</th>
<th>Leave Center Time</th>
<th>Drop Off Time</th>
{showCompletedInfo && (<th>Schedule Absent</th>)}
{showCompletedInfo && (<th>Schedule Absent Note</th>)}
@ -697,8 +800,8 @@ const PersonnelInfoTable = ({transRoutes, showCompletedInfo,
{ customer.routeType}
</td>}
<td>{customer.customer_pickup_time && new Date(customer.customer_pickup_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})}</td>
{!showCompletedInfo &&<td>{customer.customer_enter_center_time && new Date(customer.customer_enter_center_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})}</td>}
{!showCompletedInfo &&<td>{customer.customer_leave_center_time && new Date(customer.customer_leave_center_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})}</td>}
<td>{customer.customer_enter_center_time && new Date(customer.customer_enter_center_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})}</td>
<td>{customer.customer_leave_center_time && new Date(customer.customer_leave_center_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})}</td>
<td>{customer.customer_dropoff_time && new Date(customer.customer_dropoff_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})}</td>
{showCompletedInfo && (<td>
{ customer.customer_pickup_status === PICKUP_STATUS.SCHEDULE_ABSENT ? 'Yes' : "No" }
@ -749,8 +852,8 @@ const PersonnelInfoTable = ({transRoutes, showCompletedInfo,
{ customerItem.routeType}
</td>}
<td>{customerItem.customer_pickup_time && new Date(customerItem.customer_pickup_time).toLocaleTimeString('en-US', {hour12: false})}</td>
{!showCompletedInfo &&<td>{customerItem.customer_enter_center_time && new Date(customerItem.customer_enter_center_time).toLocaleTimeString('en-US', {hour12: false})}</td>}
{!showCompletedInfo &&<td>{customerItem.customer_leave_center_time && new Date(customerItem.customer_leave_center_time).toLocaleTimeString('en-US', {hour12: false})}</td>}
<td>{customerItem.customer_enter_center_time && new Date(customerItem.customer_enter_center_time).toLocaleTimeString('en-US', {hour12: false})}</td>
<td>{customerItem.customer_leave_center_time && new Date(customerItem.customer_leave_center_time).toLocaleTimeString('en-US', {hour12: false})}</td>
<td>{customerItem.customer_dropoff_time && new Date(customerItem.customer_dropoff_time).toLocaleTimeString('en-US', {hour12: false})}</td>
{showCompletedInfo && (<td>
{ customerItem.customer_pickup_status === PICKUP_STATUS.SCHEDULE_ABSENT ? 'Yes' : "No" }
@ -806,8 +909,8 @@ const PersonnelInfoTable = ({transRoutes, showCompletedInfo,
{ customer.routeType}
</td>}
<td>{customer.customer_pickup_time && new Date(customer.customer_pickup_time).toLocaleTimeString('en-US', {hour12: false})}</td>
{!showCompletedInfo &&<td>{customer.customer_enter_center_time && new Date(customer.customer_enter_center_time).toLocaleTimeString('en-US', {hour12: false})}</td>}
{!showCompletedInfo &&<td>{customer.customer_leave_center_time && new Date(customer.customer_leave_center_time).toLocaleTimeString('en-US', {hour12: false})}</td>}
<td>{customer.customer_enter_center_time && new Date(customer.customer_enter_center_time).toLocaleTimeString('en-US', {hour12: false})}</td>
<td>{customer.customer_leave_center_time && new Date(customer.customer_leave_center_time).toLocaleTimeString('en-US', {hour12: false})}</td>
<td>{customer.customer_dropoff_time && new Date(customer.customer_dropoff_time).toLocaleTimeString('en-US', {hour12: false})}</td>
{showCompletedInfo && (<td>
{ customer.customer_pickup_status === PICKUP_STATUS.SCHEDULE_ABSENT ? 'Yes' : "No" }

View File

@ -2,12 +2,15 @@ import React from "react";
import { useNavigate} from "react-router-dom";
import { ROUTE_STATUS, PICKUP_STATUS } from "./../../shared";
const RouteCard = ({transRoute, drivers, vehicles, driver, vehicle, routeIndex}) => {
const RouteCard = ({transRoute, drivers, vehicles, driver, vehicle, routeIndex, isTemplate, templateId}) => {
const navigate = useNavigate();
const params = new URLSearchParams(window.location.search);
const scheduleDate = params.get('dateSchedule');
const handleOnClick = () => {
if (scheduleDate) {
if (isTemplate && templateId) {
// For templates, use _id and navigate to template route view
navigate(`/trans-routes/daily-templates/${templateId}/view-route/${transRoute._id || transRoute.id}`);
} else if (scheduleDate) {
navigate(`/trans-routes/${transRoute.id}?dateSchedule=${scheduleDate}`);
} else {
navigate(`/trans-routes/${transRoute.id}`);
@ -72,22 +75,24 @@ const RouteCard = ({transRoute, drivers, vehicles, driver, vehicle, routeIndex})
return routeStatusData;
}
const routeStatus = getRouteStatus(transRoute);
const routeStatus = isTemplate ? { text: '', className: '' } : getRouteStatus(transRoute);
return (
<>
{
transRoute && (<div onClick={handleOnClick} className={`card-container ${routeStatus.className==='purple' ? routeStatus.className : ''}`}>
transRoute && (<div onClick={handleOnClick} className={`card-container ${!isTemplate && routeStatus.className==='purple' ? routeStatus.className : ''}`}>
<div className="route-card-title">{`#${routeIndex+1} `}{transRoute.name}</div>
<div>{drivers.find((driver) => driver.id === transRoute.driver)?.name}</div>
<div>{`${transRoute?.route_customer_list?.length} Participants`}</div>
<div>{vehicles.find((vehicle) => vehicle.id === transRoute.vehicle)?.tag}</div>
<div className="list row">
<div className="col-md-12 card-status">
<div className="float-end">{routeStatus?.text}</div>
<div className={`${routeStatus.className==='purple' ? '' : routeStatus.className} float-end`} />
{!isTemplate && (
<div className="list row">
<div className="col-md-12 card-status">
<div className="float-end">{routeStatus?.text}</div>
<div className={`${routeStatus.className==='purple' ? '' : routeStatus.className} float-end`} />
</div>
</div>
</div>
)}
</div>)
}
{

View File

@ -11,6 +11,7 @@ import { GripVertical, Pencil, RecordCircleFill, XSquare } from 'react-bootstrap
const ItemTypes = {
CARD: 'card',
UNASSIGNED_CUSTOMER: 'unassigned_customer',
};
const Card = ({ content, index, moveCard }) => {
@ -79,8 +80,9 @@ const Card = ({ content, index, moveCard }) => {
)
}
const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, viewMode, editFun}) => {
const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, viewMode, editFun, onAddCustomer = null}) => {
const [customers, setCustomers] = useState([]);
const [initializedRouteId, setInitializedRouteId] = useState(null);
const [showAddPersonnelModal, setShowAddPersonnelModal] = useState(false);
const [showAddAptGroupModal, setShowAddAptGroupModal] = useState(false);
const [showEditAptGroupModal, setShowEditAptGroupModal] = useState(false);
@ -101,16 +103,38 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
const [pageCount, setPageCount] = useState(0);
const itemsPerPage = 10;
// Helper function to get all customer IDs already in the route
const getAssignedCustomerIds = () => {
const assignedIds = new Set();
customers.forEach(item => {
if (item.customer_id) {
assignedIds.add(item.customer_id);
}
if (item.customers) {
item.customers.forEach(c => {
if (c.customer_id) {
assignedIds.add(c.customer_id);
}
});
}
});
return assignedIds;
};
useEffect(() => {
// Fetch items from another resources.
const endOffset = itemOffset + itemsPerPage;
setCurrentItems(customerOptions?.filter(customer => (lastNameFilter && (customer.lastname?.toLowerCase().indexOf(lastNameFilter) === 0)) || !lastNameFilter).filter((customer) => customer.name?.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.id.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.address1?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address2?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address3?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address4?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address5?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.apartment?.toLowerCase().includes(customerFilter.toLocaleLowerCase()) ).slice(itemOffset, endOffset));
setPageCount(Math.ceil(customerOptions?.filter(customer => (lastNameFilter && (customer.lastname?.toLowerCase().indexOf(lastNameFilter) === 0)) || !lastNameFilter).filter((customer) => customer.name.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.id.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.address1?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address2?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address3?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address4?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address5?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.apartment?.toLowerCase().includes(customerFilter.toLocaleLowerCase()) ).length / itemsPerPage));
}, [customerOptions, itemOffset, customerFilter, lastNameFilter]);
const assignedIds = getAssignedCustomerIds();
// Filter out customers already in the route
const availableCustomers = customerOptions?.filter(customer => !assignedIds.has(customer.id));
setCurrentItems(availableCustomers?.filter(customer => (lastNameFilter && (customer.lastname?.toLowerCase().indexOf(lastNameFilter) === 0)) || !lastNameFilter).filter((customer) => customer.name?.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.id.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.address1?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address2?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address3?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address4?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address5?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.apartment?.toLowerCase().includes(customerFilter.toLocaleLowerCase()) ).slice(itemOffset, endOffset));
setPageCount(Math.ceil(availableCustomers?.filter(customer => (lastNameFilter && (customer.lastname?.toLowerCase().indexOf(lastNameFilter) === 0)) || !lastNameFilter).filter((customer) => customer.name.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.id.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.address1?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address2?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address3?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address4?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address5?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.apartment?.toLowerCase().includes(customerFilter.toLocaleLowerCase()) ).length / itemsPerPage));
}, [customerOptions, itemOffset, customerFilter, lastNameFilter, customers]);
const handlePageClick = (event) => {
const newOffset = (event.selected * itemsPerPage) % customerOptions?.filter((customer) => customer.name?.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.id?.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.address1?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address2?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address3?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address4?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address5?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.apartment?.toLowerCase().includes(customerFilter.toLocaleLowerCase()) ).length;
const assignedIds = getAssignedCustomerIds();
const availableCustomers = customerOptions?.filter(customer => !assignedIds.has(customer.id));
const newOffset = (event.selected * itemsPerPage) % availableCustomers?.filter((customer) => customer.name?.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.id?.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.address1?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address2?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address3?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address4?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address5?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.apartment?.toLowerCase().includes(customerFilter.toLocaleLowerCase()) ).length;
console.log(
`User requested page number ${event.selected}, which is offset ${newOffset}`
);
@ -129,7 +153,14 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
setLastNameFilter(undefined);
if (customerOptions.length === 0) {
CustomerService.getAllActiveCustomers().then((data) => {
setCustomerOptions(data.data);
// Filter out discharged customers
const filtered = (data.data || []).filter(customer => {
const isDischarged = customer.type === 'discharged' ||
(customer.name && customer.name.toLowerCase().includes('discharged')) ||
customer.status !== 'active';
return !isDischarged;
});
setCustomerOptions(filtered);
})
}
setShowAddPersonnelModal(true);
@ -146,7 +177,14 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
setLastNameFilter(undefined);
if (customerOptions.length === 0) {
CustomerService.getAllActiveCustomers().then((data) => {
setCustomerOptions(data.data);
// Filter out discharged customers
const filtered = (data.data || []).filter(customer => {
const isDischarged = customer.type === 'discharged' ||
(customer.name && customer.name.toLowerCase().includes('discharged')) ||
customer.status !== 'active';
return !isDischarged;
});
setCustomerOptions(filtered);
})
}
setShowAddAptGroupModal(true);
@ -167,7 +205,14 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
setLastNameFilter(undefined);
if (customerOptions.length === 0) {
CustomerService.getAllActiveCustomers().then((data) => {
setCustomerOptions(data.data);
// Filter out discharged customers
const filtered = (data.data || []).filter(customer => {
const isDischarged = customer.type === 'discharged' ||
(customer.name && customer.name.toLowerCase().includes('discharged')) ||
customer.status !== 'active';
return !isDischarged;
});
setCustomerOptions(filtered);
})
}
setNewGroupAddress(group.customers[0].customer_group_address);
@ -279,6 +324,52 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
setNewRouteCustomerList([]);
}
// Function to add a customer from external drop (like unassigned customers)
const addCustomerFromDrop = useCallback((customerData, dropIndex = null) => {
if (!customerData || !customerData.id) return;
// Check if customer already exists
const customerExists = customers.some(item => {
if (item.customer_id) {
return item.customer_id === customerData.id;
}
if (item.customers) {
return item.customers.some(c => c.customer_id === customerData.id);
}
return false;
});
if (customerExists) return;
const newCustomer = {
customer_id: customerData.id,
customer_name: `${customerData.name} ${customerData.name_cn?.length > 0 ? `(${customerData.name_cn})` : ``}`,
customer_address: customerData.address1 || '',
customer_avatar: customerData.avatar,
customer_type: customerData.type,
customer_pickup_status: customerData.pickup_status,
customer_note: customerData.note,
customer_special_needs: customerData.special_needs,
customer_phone: customerData.phone || customerData.mobile_phone || customerData.home_phone,
customer_route_status: PERSONAL_ROUTE_STATUS.NO_STATUS,
customer_pickup_order: customers.length + 1,
customer_table_id: customerData.table_id,
customer_language: customerData.language
};
setCustomers(prevCustomers => {
if (dropIndex !== null && dropIndex >= 0 && dropIndex <= prevCustomers.length) {
// Insert at specific position
const newList = [...prevCustomers];
newList.splice(dropIndex, 0, newCustomer);
return newList;
} else {
// Add to end
return [...prevCustomers, newCustomer];
}
});
}, [customers]);
const addAptGroup = () => {
if (checkGroupRequiredField()) {
const result = [].concat(customers).concat([{
@ -312,9 +403,14 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
}
}
// Only initialize customers from currentRoute when the route ID changes
// Don't reset when currentRoute object reference changes (which happens on every render)
useEffect(() => {
setCustomers(getRouteCustomersWithGroups());
}, [currentRoute])
if (currentRoute?.id && currentRoute.id !== initializedRouteId) {
setCustomers(getRouteCustomersWithGroups());
setInitializedRouteId(currentRoute.id);
}
}, [currentRoute?.id, initializedRouteId])
const getRouteCustomersWithGroups = () => {
const customerList = currentRoute?.route_customer_list?.map(item => Object.assign({}, item, {routeType: currentRoute.type, routeId: currentRoute.id}));
const result = {};
@ -353,10 +449,16 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
}
const deleteCustomer = (id) => {
if (!window.confirm('Are you sure you want to remove this customer from the route?')) {
return;
}
setCustomers(customers.filter((customer) => customer.customer_id !== id));
}
const deleteGroup = (index) => {
if (!window.confirm('Are you sure you want to remove this group from the route?')) {
return;
}
const arr = [].concat(customers);
arr.splice(index, 1);
setCustomers(arr);
@ -393,7 +495,8 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
};
const ItemsGroup = ({ currentItems }) => {
return currentItems?.filter((customer) => customer.name.toLowerCase().includes(customerFilter.toLowerCase()) || customer.id.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address1?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address2?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address3?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address4?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address5?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.apartment?.toLowerCase().includes(customerFilter.toLocaleLowerCase()) ).map(
const assignedIds = getAssignedCustomerIds();
return currentItems?.filter(customer => !assignedIds.has(customer.id)).filter((customer) => customer.name.toLowerCase().includes(customerFilter.toLowerCase()) || customer.id.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address1?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address2?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address3?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address4?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address5?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.apartment?.toLowerCase().includes(customerFilter.toLocaleLowerCase()) ).map(
(customer) => <div key={customer.id} className="option-item">
<input className="me-4 mt-2" type="checkbox" checked={newRouteGroupedCustomerList.find((item) => item.customer_id === customer.id)!==undefined} value={newRouteGroupedCustomerList.find((item) => item.customer_id === customer.id)!==undefined} onChange={(e) => toggleGroupedItemToRouteList(customer, e.target.value)}/>
<div>
@ -424,6 +527,13 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
return count;
}
// Expose addCustomerFromDrop via callback
useEffect(() => {
if (onAddCustomer && typeof onAddCustomer === 'function') {
onAddCustomer(addCustomerFromDrop);
}
}, [addCustomerFromDrop, onAddCustomer]);
useEffect(() => {
const result = [];
for (const item of customers) {
@ -440,11 +550,43 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
setNewCustomerList(result);
}, [customers])
// Create a drop zone for unassigned customers
const CustomersDropZone = ({ children }) => {
const [{ isOver }, drop] = useDrop({
accept: 'UNASSIGNED_CUSTOMER',
drop: (item, monitor) => {
if (addCustomerFromDrop && item && item.id) {
addCustomerFromDrop(item, null); // Add to end
}
return { dropped: true };
},
collect: (monitor) => ({
isOver: monitor.isOver(),
}),
});
return (
<div
ref={drop}
className="customers-container mb-4"
style={{
backgroundColor: isOver ? '#f0f0f0' : 'transparent',
minHeight: isOver ? '100px' : 'auto',
border: isOver ? '2px dashed #0d6efd' : '2px dashed transparent',
borderRadius: '4px',
padding: isOver ? '8px' : '0'
}}
>
{children}
</div>
);
};
return (
<DndProvider backend={HTML5Backend}>
<>
{ !viewMode && <h6 class="text-primary">Customers Assigned ({getCurrentAssignedNumber()})</h6>}
{ viewMode && <h6 class="text-primary">Route Assignment <button className="btn btn-sm btn-primary" onClick={() => editFun('assignment')}><Pencil size={16} className="me-2"></Pencil>Edit </button></h6>}
{!viewMode && <div className="customers-container mb-4">
{!viewMode && <CustomersDropZone>
{customers.map((item, index) => {
if (item?.customers) {
return <Card key={index} index={index} moveCard={reorderItems} content={(<div className="customers-dnd-item-container">
@ -481,12 +623,12 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
<div className="new-customers-dnd-item-container">
<div className="stop-index"><span>{`Stop ${customers?.length+1}`}</span><RecordCircleFill size={16} color={"#ccc"} className="ms-2"></RecordCircleFill> </div>
<div>
<button className="btn btn-primary btn-sm me-2" onClick={() => openAddPersonnelModal()}> + Add Personnel </button>
<button className="btn btn-primary btn-sm me-2" onClick={() => openAddAptGroupModal()}> + Add Apt Group </button>
<button className="btn btn-primary btn-sm me-2 mb-2" onClick={() => openAddPersonnelModal()}> + Add Personnel </button>
<button className="btn btn-primary btn-sm me-2 mb-2" onClick={() => openAddAptGroupModal()}> + Add Apt Group </button>
</div>
</div>
</div>}
</CustomersDropZone>}
{
viewMode && <div className="customers-container mb-4">
{customers.map((item, index) => {
@ -718,7 +860,7 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
</Button>
</Modal.Footer>
</Modal>
</DndProvider>
</>
);
};

View File

@ -1,4 +1,4 @@
import React, {useEffect, useState} from "react";
import React, {useEffect, useState, useRef} from "react";
import { useSelector,useDispatch } from "react-redux";
import { useParams, useNavigate } from "react-router-dom";
import { selectAllRoutes, transRoutesSlice, vehicleSlice, selectTomorrowAllRoutes, selectAllActiveDrivers, selectAllActiveVehicles, selectHistoryRoutes } from "./../../store";
@ -10,6 +10,9 @@ import 'react-time-picker/dist/TimePicker.css';
import moment from 'moment';
import { Archive, GripVertical } from "react-bootstrap-icons";
import { PERSONAL_ROUTE_STATUS } from "../../shared";
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useDrag } from 'react-dnd';
const RouteEdit = () => {
const params = useParams();
@ -38,6 +41,7 @@ const RouteEdit = () => {
const [currentRoute, setCurrentRoute] = useState(undefined);
const [allCustomers, setAllCustomers] = useState([]);
const [unassignedCustomers, setUnassignedCustomers] = useState([]);
const [addCustomerToRoute, setAddCustomerToRoute] = useState(null);
const paramsQuery = new URLSearchParams(window.location.search);
const scheduleDate = paramsQuery.get('dateSchedule');
const editSection = paramsQuery.get('editSection')
@ -54,17 +58,43 @@ const RouteEdit = () => {
}
const softDeleteCurrentRoute = () => {
if (!window.confirm('Are you sure you want to archive this route? This action cannot be undone.')) {
return;
}
const data = Object.assign({}, currentRoute, {status: ['disabled']})
dispatch(updateRoute({ id: currentRoute?.id, data, callback: redirectToDashboard }));
// redirectToDashboard();
}
const validateRoute = () => {
const errors = [];
// Required fields validation
if (!routeName || routeName.trim() === '') {
errors.push('Route Name');
}
if (!newRouteType || newRouteType === '') {
errors.push('Route Type');
}
if (!newDriver || newDriver === '') {
errors.push('Driver');
}
if (!newVehicle || newVehicle === '') {
errors.push('Vehicle');
}
if (errors.length > 0) {
window.alert(`Please fill in the following required fields:\n${errors.join('\n')}`);
return false;
}
return true;
};
const updateCurrentRoute = () => {
try {
if (!routeName || routeName === '') { setErrorMessage('Route Name is Required'); return;}
if (!newRouteType || newRouteType === '') {setErrorMessage('Route Type is Required'); return;}
if (!newDriver || newDriver === '') {setErrorMessage('Driver is Required'); return;}
if (!newVehicle || newVehicle === '') {setErrorMessage('Vehicle is Required'); return;}
if (!validateRoute()) {
return;
}
let data = Object.assign({}, currentRoute, {name: routeName, driver: newDriver, vehicle: newVehicle, type: newRouteType, route_customer_list: newCustomerList});
if (estimatedStartTime && estimatedStartTime !== '') {
data = Object.assign({}, data, {estimated_start_time: combineDateAndTime(currentRoute.schedule_date, estimatedStartTime)})
@ -146,9 +176,15 @@ const RouteEdit = () => {
});
// Filter out customers that are not assigned to any route
return customers.filter(customer =>
customer.status === 'active' && !assignedCustomerIds.has(customer.id)
);
// Also exclude discharged customers (type is 'discharged' or name contains 'discharged')
return customers.filter(customer => {
const isDischarged = customer.type === 'discharged' ||
(customer.name && customer.name.toLowerCase().includes('discharged')) ||
(customer.status !== 'active');
return customer.status === 'active' &&
!assignedCustomerIds.has(customer.id) &&
!isDischarged;
});
}
useEffect(() => {
@ -180,16 +216,22 @@ const RouteEdit = () => {
const routeDate = currentRoute.schedule_date;
// Get routes from the same date as the current route
// Get routes from the same date as the current route (excluding current route which we'll handle separately)
const sameDateRoutes = [
...allRoutes.filter(route => route.schedule_date === routeDate),
...tomorrowRoutes.filter(route => route.schedule_date === routeDate),
...historyRoutes.filter(route => route.schedule_date === routeDate)
...allRoutes.filter(route => route.schedule_date === routeDate && route.id !== currentRoute.id),
...tomorrowRoutes.filter(route => route.schedule_date === routeDate && route.id !== currentRoute.id),
...historyRoutes.filter(route => route.schedule_date === routeDate && route.id !== currentRoute.id)
];
const unassigned = calculateUnassignedCustomers(allCustomers, sameDateRoutes);
// Add a virtual route with the current newCustomerList (to include customers being added in the editor)
const routesWithCurrentEdits = [
...sameDateRoutes,
{ route_customer_list: newCustomerList || [] }
];
const unassigned = calculateUnassignedCustomers(allCustomers, routesWithCurrentEdits);
setUnassignedCustomers(unassigned);
}, [allCustomers, allRoutes, tomorrowRoutes, historyRoutes, currentRoute]);
}, [allCustomers, allRoutes, tomorrowRoutes, historyRoutes, currentRoute, newCustomerList]);
// useEffect(() => {
// if (currentRoute) {
@ -203,14 +245,41 @@ const RouteEdit = () => {
// setErrorMessage(undefined);
// }, [currentRoute])
// Draggable component for unassigned customers
const DraggableUnassignedCustomer = ({ customer }) => {
const [{ isDragging }, drag] = useDrag({
type: 'UNASSIGNED_CUSTOMER',
item: () => customer,
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
const opacity = isDragging ? 0.5 : 1;
return (
<div ref={drag} style={{ opacity }} className="customers-dnd-item-container-absent">
<GripVertical className="me-4" size={14}></GripVertical>
<div className="customer-dnd-item">
<span>{customer.name} </span>
<small className="me-2">{customer.address1}</small>
<small className="me-2">{customer.type}</small>
</div>
</div>
);
};
return (
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Transportation</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/">Transportation</Breadcrumb.Item>
<Breadcrumb.Item href="/trans-routes/dashboard">
Transportation Routes
</Breadcrumb.Item>
<Breadcrumb.Item active>
Edit Route
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>
@ -219,7 +288,7 @@ const RouteEdit = () => {
</div>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-container form-page">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="routeOverview" id="route-view-tab">
<Tab eventKey="routeOverview" title="Route Information">
@ -342,7 +411,7 @@ const RouteEdit = () => {
{
newDriver && newDriver !== '' && <div className="column-card">
<h6 className="text-primary">Driver Information</h6>
<div className="text-primary">Personal Details</div>
<small className="text-primary">Personal Details</small>
<div className="app-main-content-fields-section short">
<div className="field-body">
<div className="field-label">Driver Name</div>
@ -357,25 +426,26 @@ const RouteEdit = () => {
<div className="field-value">{drivers.find(item => item.id === newDriver)?.title}</div>
</div>
<div className="field-body">
<div className="field-label">Job Status</div>
<div className="field-value">{vehicles.find(item => item.id === newVehicle)?.employment_status}</div>
<div className="field-label">Job Type</div>
<div className="field-value">{drivers.find(item => item.id === newDriver)?.employment_status}</div>
</div>
<div className="field-body">
<div className="field-label">License Type</div>
<div className="field-value">{drivers.find(item => item.id === newDriver)?.license_type}</div>
</div>
</div>
<div className="app-main-content-fields-section short">
<div className="field-body">
<div className="field-label">Driver Capacity</div>
<div className="field-value">{drivers.find(item => item.id === newDriver)?.driver_capacity}</div>
</div>
<div className="field-body">
<div className="field-label">Phone Number</div>
<div className="field-value">{drivers.find(item => item.id === newDriver)?.phone}</div>
</div>
<div className="field-body">
<div className="field-label">Email</div>
<div className="field-value">{vehicles.find(item => item.id === newVehicle)?.email}</div>
<div className="field-value">{drivers.find(item => item.id === newDriver)?.email}</div>
</div>
<div className="field-body"></div>
<div className="field-body"></div>
<div className="field-body"></div>
</div>
</div>
}
@ -383,67 +453,63 @@ const RouteEdit = () => {
</div> }
{
editSection === 'assignment' && <div className="multi-columns-container">
<div className="column-container">
<div className="column-card adjust">
<div className="col-md-12 mb-4">
<RouteCustomerEditor
currentRoute={currentRoute ? {
...currentRoute,
route_customer_list: currentRoute.route_customer_list?.filter(
customer => customer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT
) || []
} : undefined}
setNewCustomerList={setNewCustomerList}
/>
editSection === 'assignment' && <DndProvider backend={HTML5Backend}>
<div className="multi-columns-container">
<div className="column-container">
<div className="column-card adjust" style={{paddingRight: '30px'}}>
<div className="col-md-12 mb-4">
<RouteCustomerEditor
currentRoute={currentRoute ? {
...currentRoute,
route_customer_list: currentRoute.route_customer_list?.filter(
customer => customer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT
) || []
} : undefined}
setNewCustomerList={setNewCustomerList}
onAddCustomer={(addFn) => setAddCustomerToRoute(() => addFn)}
/>
</div>
</div>
</div>
</div>
<div className="column-container">
<div className="column-card adjust">
<h6 className="text-primary">Scheduled Absences ({currentRoute?.route_customer_list?.filter(item => item?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT)?.length || 0})</h6>
<div className="customers-container mb-4">
{
currentRoute?.route_customer_list.filter(customer => customer?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT)?.map((abItem) => {
return <div className="customers-dnd-item-container-absent">
<GripVertical className="me-4" size={14}></GripVertical>
<div className="customer-dnd-item">
<span>{abItem.customer_name} </span>
<small className="me-2">{abItem.customer_address}</small>
<small className="me-2">{abItem.customer_pickup_status}</small>
<div className="column-container">
<div className="column-card adjust">
<h6 className="text-primary">Scheduled Absences ({currentRoute?.route_customer_list?.filter(item => item?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT)?.length || 0})</h6>
<div className="customers-container mb-4">
{
currentRoute?.route_customer_list.filter(customer => customer?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT)?.map((abItem) => {
return <div key={abItem.customer_id} className="customers-dnd-item-container-absent">
<GripVertical className="me-4" size={14}></GripVertical>
<div className="customer-dnd-item">
<span>{abItem.customer_name} </span>
<small className="me-2">{abItem.customer_address}</small>
<small className="me-2">{abItem.customer_pickup_status}</small>
</div>
</div>
</div>
})
}
})
}
</div>
</div>
</div>
<div className="column-container">
<div className="column-card adjust">
<h6 className="text-primary">Unassigned Customers ({unassignedCustomers?.length || 0})</h6>
<div className="customers-container mb-4">
{
unassignedCustomers?.map((customer) => {
return <DraggableUnassignedCustomer key={customer.id} customer={customer} />
})
}
</div>
</div>
</div>
</div>
<div className="column-container">
<div className="column-card adjust">
<h6 className="text-primary">Unassigned Customers ({unassignedCustomers?.length || 0})</h6>
<div className="customers-container mb-4">
{
unassignedCustomers?.map((customer) => {
return <div className="customers-dnd-item-container-absent">
<GripVertical className="me-4" size={14}></GripVertical>
<div className="customer-dnd-item">
<span>{customer.name} </span>
<small className="me-2">{customer.address1}</small>
<small className="me-2">{customer.type}</small>
</div>
</div>
})
}
</div>
</div>
</div>
</div>
</DndProvider>
}
</Tab>
</Tabs>
<div className="list-func-panel">
<button className="btn btn-primary" onClick={() => softDeleteCurrentRoute()}><Archive size={16} className="me-2"></Archive>Archive</button>
<button className="btn btn-primary" onClick={() => softDeleteCurrentRoute()}><Archive size={16} className="me-2"></Archive>Delete</button>
</div>
</div>
</div>

View File

@ -76,6 +76,13 @@ const RouteReportWithSignature = () => {
}
});
}, [currentRoute]);
// Get checklist result from route
const checklistResult = currentRoute?.checklist_result || [];
const getChecklistStatus = (itemKey) => {
const item = checklistResult.find(c => c.key === itemKey || c.label?.toLowerCase().includes(itemKey.toLowerCase()));
return item?.checked ? '✓' : '';
};
return (
<>
<style>
@ -85,9 +92,50 @@ const RouteReportWithSignature = () => {
min-width: auto !important;
width: auto !important;
max-width: none !important;
border: 1px solid #333;
padding: 4px 8px;
font-size: 12px;
}
.route-report-table {
table-layout: auto !important;
border-collapse: collapse;
}
.route-report-table thead th {
background-color: #f5f5f5;
text-align: center;
color: #000;
}
.route-report-header {
border: 1px solid #333;
padding: 8px 16px;
margin-bottom: 8px;
font-size: 14px;
}
.route-report-signature-row {
border: 1px solid #333;
padding: 8px 16px;
margin-bottom: 8px;
font-size: 14px;
}
.inspection-table {
border-collapse: collapse;
width: 100%;
margin-top: 16px;
}
.inspection-table th,
.inspection-table td {
border: 1px solid #333;
padding: 4px 8px;
font-size: 11px;
}
.inspection-table th {
background-color: #f5f5f5;
color: #000;
}
.bilingual-label {
display: block;
font-size: 10px;
color: #666;
}
`}
</style>
@ -96,116 +144,210 @@ const RouteReportWithSignature = () => {
<button className="btn btn-link btn-sm" onClick={() => directToView()}>Back</button>
</div>
</div>
<div>
<div className="float-end" style={{'background': '#eee', padding: '12px 8px', width: '50px', height: '50px', textAlign: 'center'}}>
<span>{routeIndex}</span>
{/* Header Row with Route Info */}
<div className="route-report-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ flex: 1 }}>
<strong>Route (路线):</strong> {currentRoute?.name}
</div>
<div style={{ flex: 1 }}>
<strong>{driverLabel} ({isByOwnRoute ? '工作人员' : '司机'}):</strong> {currentDriver?.name}
</div>
<div style={{ flex: 1 }}>
<strong>Vehicle (车号):</strong> {currentVehicle?.vehicle_number}
</div>
<div style={{ flex: 1 }}>
<strong>Date (日期):</strong> {currentRoute?.schedule_date && moment(currentRoute?.schedule_date).format('MM/DD/YYYY')}
</div>
<div style={{ background: '#eee', padding: '8px 12px', fontWeight: 'bold' }}>
{routeIndex}
</div>
</div>
<div className="list row mb-2">
<div className="col-md-3 col-sm-6 col-xs-12 mb-4">
Route: <span className="report-field-value"><strong>{currentRoute?.name}</strong></span>
</div>
<div className="col-md-3 col-sm-6 col-xs-12 mb-4">
{driverLabel}: <span className="report-field-value"><strong>{currentDriver?.name}</strong></span>
</div>
<div className="col-md-3 col-sm-6 col-xs-12 mb-4">
Vehicle: <span className="report-field-value"><strong>{currentVehicle?.vehicle_number}</strong></span>
</div>
<div className="col-md-3 col-sm-6 col-xs-12 mb-4">
Date: <span className="report-field-value"><strong>{currentRoute?.schedule_date && (new Date(currentRoute?.schedule_date))?.toLocaleDateString()}</strong></span>
</div>
</div>
<div className="list row">
<div className="col-md-6 col-sm-12 mb-4">
{driverLabel}'s Signature: {signature && <span className="mb-2 me-4">{signature && <img width="100px" src={`data:image/jpg;base64, ${signature}`}/>}</span>} {currentRoute?.end_time && <span>{new Date(currentRoute?.end_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})}</span>}
{/* Signature Row */}
<div className="route-report-signature-row" style={{ display: 'flex', justifyContent: 'space-between' }}>
<div style={{ flex: 1 }}>
<strong>{driverLabel}'s Signature ({isByOwnRoute ? '工作人员签字' : '司机签字'}):</strong>
{signature && <img width="100px" src={`data:image/jpg;base64, ${signature}`} style={{ marginLeft: '16px' }} alt="Driver Signature" />}
{currentRoute?.end_time && <span style={{ marginLeft: '16px' }}>{new Date(currentRoute?.end_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})}</span>}
</div>
<div className="col-md-6 col-sm-12 mb-4">
{/* Manager's Signature (经理签字): {directorSignature && <span className="mb-2">{directorSignature && <img width="100px" src={`data:image/jpg;base64, ${directorSignature}`}/>}</span>} */}
Manager's Signature: <img width="100px" src="/images/signature.jpeg"/>
<div style={{ flex: 1 }}>
<strong>Manager's Signature (经理签字):</strong>
<img width="100px" src="/images/signature.jpeg" style={{ marginLeft: '16px' }} alt="Manager Signature" />
</div>
</div>
{/* Main Customer Table */}
<div className="list row">
<div className="col-md-12 mb-4">
<table className="personnel-info-table route-report-table" style={{
tableLayout: 'auto',
width: '100%'
}}>
<table className="personnel-info-table route-report-table" style={{ tableLayout: 'auto', width: '100%' }}>
<thead>
{/* English Header Row */}
<tr>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}>No.</th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}>Name</th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}>Phone</th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}>Address</th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}>Unit</th>
<th colSpan={2} style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}>Pick-Up</th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}>Pick-Up</th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}>Arrival</th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}>Departure</th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}>Drop-Off</th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}>Notice</th>
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>No.<span className="bilingual-label">序号</span></th>
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Name<span className="bilingual-label">姓名</span></th>
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Phone<span className="bilingual-label">联系电话</span></th>
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Address<span className="bilingual-label">地址</span></th>
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Unit<span className="bilingual-label">房间号</span></th>
<th colSpan={2} style={{ textAlign: 'center' }}>Attendance<span className="bilingual-label">出勤</span></th>
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Pick-Up<span className="bilingual-label">接到时间</span></th>
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Arrival<span className="bilingual-label">抵达中心</span></th>
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Departure<span className="bilingual-label">离开中心</span></th>
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Drop-Off<span className="bilingual-label">送达时间</span></th>
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Notice<span className="bilingual-label">备注</span></th>
</tr>
<tr>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}></th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}></th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}></th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}></th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}></th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}>Y</th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}>N</th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}></th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}></th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}></th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}></th>
<th style={{ minWidth: 'auto !important', width: 'auto !important', maxWidth: 'none !important' }}></th>
<th style={{ textAlign: 'center' }}>Y</th>
<th style={{ textAlign: 'center' }}>N</th>
</tr>
</thead>
<tbody>
{
currentRoute?.route_customer_list?.map((customer, index) => {
const relativeRouteCustomer = getInboundOrOutboundRouteCustomer(customer?.customer_id);
// console.log('cust', relativeRouteCustomer);
const otherRouteWithThisCustomer = getOtherRouteWithThisCustomer(customer?.customer_id);
// console.log('other', otherRouteWithThisCustomer)
const customerInOtherRoute = otherRouteWithThisCustomer?.route_customer_list?.find(cust => cust?.customer_id === customer?.customer_id);
// console.log('other user', customerInOtherRoute)
const otherOutboundRoute = getOtherOutboundRouteWithThisCustomer(customer?.customer_id);
return (<tr>
<td>{index+1}</td>
<td>{customer?.customer_name}</td>
<td>{customer?.customer_phone}</td>
<td>{customer?.customer_address_override || customer?.customer_address}</td>
<td></td>
<td>{ ![PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT, PERSONAL_ROUTE_STATUS.UNEXPECTED_ABSENT].includes(relativeRouteCustomer?.customer_route_status) && ![PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT, PERSONAL_ROUTE_STATUS.UNEXPECTED_ABSENT].includes(customer?.customer_route_status) ? "✓" : ''}</td>
<td>{![PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT, PERSONAL_ROUTE_STATUS.UNEXPECTED_ABSENT].includes(relativeRouteCustomer?.customer_route_status) && ![PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT, PERSONAL_ROUTE_STATUS.UNEXPECTED_ABSENT].includes(customer?.customer_route_status) ? "" : 'x'}</td>
<td>{customer?.customer_pickup_time ? new Date(customer?.customer_pickup_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'}) : (relativeRouteCustomer?.customer_pickup_time? new Date(relativeRouteCustomer?.customer_pickup_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'}) : (customerInOtherRoute?.customer_pickup_time ? new Date(customerInOtherRoute?.customer_pickup_time)?.toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'}): ''))}</td>
<td>{customer?.customer_enter_center_time ? new Date(customer?.customer_enter_center_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'}) : (relativeRouteCustomer?.customer_enter_center_time ? new Date(relativeRouteCustomer?.customer_enter_center_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'}) : (customerInOtherRoute?.customer_enter_center_time ? new Date(customerInOtherRoute?.customer_enter_center_time)?.toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'}): ''))}</td>
<td>{customer?.customer_leave_center_time && customer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SKIP_DROPOFF ? new Date(customer?.customer_leave_center_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'}) : (relativeRouteCustomer?.customer_leave_center_time && relativeRouteCustomer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SKIP_DROPOFF ? new Date(relativeRouteCustomer?.customer_leave_center_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'}) : (customerInOtherRoute?.customer_leave_center_time ? new Date(customerInOtherRoute?.customer_leave_center_time)?.toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'}): ''))}</td>
<td>{customer?.customer_dropoff_time && customer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SKIP_DROPOFF ? new Date(customer?.customer_dropoff_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'}) : (relativeRouteCustomer?.customer_dropoff_time && relativeRouteCustomer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SKIP_DROPOFF ? new Date(relativeRouteCustomer?.customer_dropoff_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'}) : (customerInOtherRoute?.customer_dropoff_time ? new Date(customerInOtherRoute?.customer_dropoff_time)?.toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'}): ''))}</td>
<td>
{customer?.customer_type !== CUSTOMER_TYPE.MEMBER && <div>{ CUSTOMER_TYPE_TEXT[customer?.customer_type]}</div> }
{ !relativeRouteCustomer && otherRouteWithThisCustomer && (
<div>
{`${currentRoute?.type === 'inbound' ? 'Switch to Route ' : 'Switch from Route '} ${otherRouteWithThisCustomer?.name}`}
</div>
)
}
{
customer?.customer_route_status === PERSONAL_ROUTE_STATUS.SKIP_DROPOFF && otherOutboundRoute && (
<div>
{`Switch to Route ${otherOutboundRoute?.name}`}
</div>
)
}
</td>
</tr>)
const isAttending = ![PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT, PERSONAL_ROUTE_STATUS.UNEXPECTED_ABSENT].includes(relativeRouteCustomer?.customer_route_status) &&
![PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT, PERSONAL_ROUTE_STATUS.UNEXPECTED_ABSENT].includes(customer?.customer_route_status);
return (
<tr key={index}>
<td style={{ textAlign: 'center' }}>{index + 1}</td>
<td>{customer?.customer_name}</td>
<td>{customer?.customer_phone}</td>
<td>{customer?.customer_address_override || customer?.customer_address}</td>
<td></td>
<td style={{ textAlign: 'center' }}>{isAttending ? '✓' : ''}</td>
<td style={{ textAlign: 'center' }}>{!isAttending ? '✗' : ''}</td>
<td style={{ textAlign: 'center' }}>
{customer?.customer_pickup_time
? new Date(customer?.customer_pickup_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
: (relativeRouteCustomer?.customer_pickup_time
? new Date(relativeRouteCustomer?.customer_pickup_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
: (customerInOtherRoute?.customer_pickup_time
? new Date(customerInOtherRoute?.customer_pickup_time)?.toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
: ''))}
</td>
<td style={{ textAlign: 'center' }}>
{customer?.customer_enter_center_time
? new Date(customer?.customer_enter_center_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
: (relativeRouteCustomer?.customer_enter_center_time
? new Date(relativeRouteCustomer?.customer_enter_center_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
: (customerInOtherRoute?.customer_enter_center_time
? new Date(customerInOtherRoute?.customer_enter_center_time)?.toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
: ''))}
</td>
<td style={{ textAlign: 'center' }}>
{customer?.customer_leave_center_time && customer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SKIP_DROPOFF
? new Date(customer?.customer_leave_center_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
: (relativeRouteCustomer?.customer_leave_center_time && relativeRouteCustomer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SKIP_DROPOFF
? new Date(relativeRouteCustomer?.customer_leave_center_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
: (customerInOtherRoute?.customer_leave_center_time
? new Date(customerInOtherRoute?.customer_leave_center_time)?.toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
: ''))}
</td>
<td style={{ textAlign: 'center' }}>
{customer?.customer_dropoff_time && customer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SKIP_DROPOFF
? new Date(customer?.customer_dropoff_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
: (relativeRouteCustomer?.customer_dropoff_time && relativeRouteCustomer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SKIP_DROPOFF
? new Date(relativeRouteCustomer?.customer_dropoff_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
: (customerInOtherRoute?.customer_dropoff_time
? new Date(customerInOtherRoute?.customer_dropoff_time)?.toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
: ''))}
</td>
<td>
{customer?.customer_type !== CUSTOMER_TYPE.MEMBER && <div>{CUSTOMER_TYPE_TEXT[customer?.customer_type]}</div>}
{!relativeRouteCustomer && otherRouteWithThisCustomer && (
<div>{`${currentRoute?.type === 'inbound' ? 'Switch to Route ' : 'Switch from Route '} ${otherRouteWithThisCustomer?.name}`}</div>
)}
{customer?.customer_route_status === PERSONAL_ROUTE_STATUS.SKIP_DROPOFF && otherOutboundRoute && (
<div>{`Switch to Route ${otherOutboundRoute?.name}`}</div>
)}
{/* Rest stop check for inbound */}
{currentRoute?.type === 'inbound' && (() => {
const pickupTime = customer?.customer_pickup_time || relativeRouteCustomer?.customer_pickup_time || customerInOtherRoute?.customer_pickup_time;
const enterTime = customer?.customer_enter_center_time || relativeRouteCustomer?.customer_enter_center_time || customerInOtherRoute?.customer_enter_center_time;
if (pickupTime && enterTime) {
const diffMs = new Date(enterTime) - new Date(pickupTime);
const diffHours = diffMs / (1000 * 60 * 60);
if (diffHours > 1) return <div>Rest Stop</div>;
}
return null;
})()}
{/* Rest stop check for outbound */}
{currentRoute?.type === 'outbound' && (() => {
const leaveTime = customer?.customer_leave_center_time || relativeRouteCustomer?.customer_leave_center_time || customerInOtherRoute?.customer_leave_center_time;
const dropoffTime = customer?.customer_dropoff_time || relativeRouteCustomer?.customer_dropoff_time || customerInOtherRoute?.customer_dropoff_time;
if (leaveTime && dropoffTime) {
const diffMs = new Date(dropoffTime) - new Date(leaveTime);
const diffHours = diffMs / (1000 * 60 * 60);
if (diffHours > 1) return <div>Rest Stop</div>;
}
return null;
})()}
</td>
</tr>
);
})
}
</tbody>
</table>
</div>
</div>
{/* Vehicle Inspection Checklist */}
<div className="list row">
<div className="col-md-12">
<table className="inspection-table">
<thead>
<tr>
<th>Item</th>
<th>Inspected</th>
<th>Item</th>
<th>Inspected</th>
<th>Item</th>
<th>Inspected</th>
</tr>
</thead>
<tbody>
<tr>
<td>Tires/Rims<span className="bilingual-label">车胎/车圈</span></td>
<td style={{ textAlign: 'center' }}>{getChecklistStatus('tires') || getChecklistStatus('Tires')}</td>
<td>Door(s)<span className="bilingual-label">车门</span></td>
<td style={{ textAlign: 'center' }}>{getChecklistStatus('door') || getChecklistStatus('Door')}</td>
<td>Seat/Seat-belt<span className="bilingual-label">座位/安全带</span></td>
<td style={{ textAlign: 'center' }}>{getChecklistStatus('seat') || getChecklistStatus('Seat')}</td>
</tr>
<tr>
<td>Mirrors/Windows<span className="bilingual-label">镜子/窗户</span></td>
<td style={{ textAlign: 'center' }}>{getChecklistStatus('mirror') || getChecklistStatus('Mirror') || getChecklistStatus('window') || getChecklistStatus('Window')}</td>
<td>Light(s)<span className="bilingual-label">车灯</span></td>
<td style={{ textAlign: 'center' }}>{getChecklistStatus('light') || getChecklistStatus('Light')}</td>
<td>Lift (wheelchair)<span className="bilingual-label">轮椅起降器</span></td>
<td style={{ textAlign: 'center' }}>{getChecklistStatus('lift') || getChecklistStatus('Lift')}</td>
</tr>
<tr>
<td>Emergency Folder<span className="bilingual-label">紧急情况信息夹</span></td>
<td style={{ textAlign: 'center' }}>{getChecklistStatus('emergency') || getChecklistStatus('Emergency')}</td>
<td>Tie Down<span className="bilingual-label">固定绳索</span></td>
<td style={{ textAlign: 'center' }}>{getChecklistStatus('tie') || getChecklistStatus('Tie')}</td>
<td>Heater/AC<span className="bilingual-label">暖气/冷气</span></td>
<td style={{ textAlign: 'center' }}>{getChecklistStatus('heater') || getChecklistStatus('Heater') || getChecklistStatus('ac') || getChecklistStatus('AC')}</td>
</tr>
<tr>
<td>Fire Extinguisher<span className="bilingual-label">灭火器</span></td>
<td style={{ textAlign: 'center' }}>{getChecklistStatus('fire') || getChecklistStatus('Fire') || getChecklistStatus('extinguisher')}</td>
<td>Body<span className="bilingual-label">车身</span></td>
<td style={{ textAlign: 'center' }}>{getChecklistStatus('body') || getChecklistStatus('Body')}</td>
<td>Coolant<span className="bilingual-label">冷冻液</span></td>
<td style={{ textAlign: 'center' }}>{getChecklistStatus('coolant') || getChecklistStatus('Coolant')}</td>
</tr>
</tbody>
</table>
</div>
</div>
</>
);
};

View File

@ -89,10 +89,13 @@ const RouteView = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Transportation</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/">Transportation</Breadcrumb.Item>
<Breadcrumb.Item href="/trans-routes/dashboard">
Transportation Routes
</Breadcrumb.Item>
<Breadcrumb.Item active>
View Route
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>
@ -100,7 +103,7 @@ const RouteView = () => {
</h4>
</div>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-container form-page">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="routeOverview" id="route-view-tab">
<Tab eventKey="routeOverview" title="Route Information">

View File

@ -1,15 +1,12 @@
import React, { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useNavigate, useSearchParams } from "react-router-dom";
import { selectInboundRoutes, selectTomorrowAllRoutes, selectTomorrowInboundRoutes, selectTomorrowOutboundRoutes, selectHistoryInboundRoutes, selectHistoryRoutes, selectHistoryOutboundRoutes, selectOutboundRoutes, selectAllBreakfastRecords, selectAllLunchRecords, selectAllSnackRecords, selectAllRoutes, selectAllActiveVehicles, selectAllActiveDrivers, transRoutesSlice } from "./../../store";
import { selectInboundRoutes, selectTomorrowAllRoutes, selectTomorrowInboundRoutes, selectTomorrowOutboundRoutes, selectHistoryInboundRoutes, selectHistoryRoutes, selectHistoryOutboundRoutes, selectOutboundRoutes, selectAllRoutes, selectAllActiveVehicles, selectAllActiveDrivers, transRoutesSlice } from "./../../store";
import RoutesSection from "./RoutesSection";
import PersonnelSection from "./PersonnelSection";
import { AuthService, CustomerService, TransRoutesService, DriverService, EventsService, SignatureRequestService } from "../../services";
import { AuthService, CustomerService, TransRoutesService, DriverService, EventsService, SignatureRequestService, DailyRoutesTemplateService } from "../../services";
import { PERSONAL_ROUTE_STATUS, ROUTE_STATUS, reportRootUrl, CUSTOMER_TYPE_TEXT, PERSONAL_ROUTE_STATUS_TEXT, PICKUP_STATUS, PICKUP_STATUS_TEXT, REPORT_TYPE } from "../../shared";
import BreakfastSection from './BreakfastSection';
import moment from 'moment';
import LunchSection from "./LunchSection";
import SnackSection from "./SnackSection";
import DatePicker from "react-datepicker";
import { CalendarWeek, ClockHistory, Copy, Download, Eraser, Plus, Clock, Send, Filter, CalendarCheck, Check } from "react-bootstrap-icons";
import { Breadcrumb, Tabs, Tab, Dropdown, Spinner, Modal, Button } from "react-bootstrap";
@ -19,7 +16,7 @@ import RouteCustomerTable from "./RouteCustomerTable";
const RoutesDashboard = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const { fetchAllRoutes, createRoute, fetchAllBreakfastRecords, fetchAllLunchRecords, fetchAllSnackRecords, fetchAllHistoryRoutes, fetchAllTomorrowRoutes, updateRoute } = transRoutesSlice.actions;
const { fetchAllRoutes, createRoute, fetchAllHistoryRoutes, fetchAllTomorrowRoutes, updateRoute } = transRoutesSlice.actions;
const inboundRoutes = useSelector(selectInboundRoutes);
const outboundRoutes = useSelector(selectOutboundRoutes);
const tmrInboundRoutes = useSelector(selectTomorrowInboundRoutes);
@ -31,10 +28,8 @@ const RoutesDashboard = () => {
const allTomorrowRoutes = useSelector(selectTomorrowAllRoutes);
const drivers = useSelector(selectAllActiveDrivers);
const vehicles = useSelector(selectAllActiveVehicles);
const breakfastRecords = useSelector(selectAllBreakfastRecords);
const lunchRecords = useSelector(selectAllLunchRecords);
const snackRecords = useSelector(selectAllSnackRecords);
const [showSyncCustomersLoading, setShowSyncCustomersLoading] = useState(false);
const [hasAutoSynced, setHasAutoSynced] = useState(false);
const [keyword, setKeyword] = useState('');
const [directorSignature, setDirectorSignature] = useState(undefined);
@ -75,6 +70,16 @@ const RoutesDashboard = () => {
const [customerNameFilter, setCustomerNameFilter] = useState('');
const [customerTableId, setCustomerTableId] = useState('');
const [routeTypeFilter, setRouteTypeFilter] = useState('');
// Save template modal state
const [showSaveTemplateModal, setShowSaveTemplateModal] = useState(false);
const [templateName, setTemplateName] = useState('');
const [savingTemplate, setSavingTemplate] = useState(false);
// Apply template state
const [templates, setTemplates] = useState([]);
const [selectedTemplateId, setSelectedTemplateId] = useState('');
const [applyingTemplate, setApplyingTemplate] = useState(false);
const params = new URLSearchParams(window.location.search);
@ -152,6 +157,13 @@ const RoutesDashboard = () => {
setDirectorSignature(data?.data)
}
});
// Fetch all daily routes templates
DailyRoutesTemplateService.getAll().then((response) => {
setTemplates(response.data || []);
}).catch(err => {
console.error('Error fetching templates:', err);
});
}, []);
useEffect(() => {
@ -188,6 +200,12 @@ const RoutesDashboard = () => {
setRoutesForShowing(allRoutes);
setRoutesInboundForShowing(inboundRoutes);
setRoutesOutboundForShowing(processRoutesForAbsentCustomers(inboundRoutes, outboundRoutes));
// Auto-sync customer info for today's routes when they first load (only once)
if (allRoutes && allRoutes.length > 0 && !hasAutoSynced && !showSyncCustomersLoading) {
setHasAutoSynced(true);
syncCustomersInfo();
}
} else {
if (dateSelected > new Date()) {
setRoutesForShowing(allTomorrowRoutes);
@ -240,6 +258,7 @@ const RoutesDashboard = () => {
setDateSelected(new Date());
setOriginDateSelected(undefined);
setTargetedDateSelected(undefined);
setHasAutoSynced(false); // Reset auto-sync flag when changing tabs
// setSelectedDriver(undefined);
}
@ -537,6 +556,12 @@ const RoutesDashboard = () => {
}
setTimeout(dispatch(fetchAllRoutes()), 10000);
}
const handleCleanAllRoutesStatus = () => {
if (window.confirm('Are you sure you want to do this? This cannot be undone.')) {
cleanAllRoutesStatus();
}
}
const syncCustomersInfo = () => {
setShowSyncCustomersLoading(true);
CustomerService.getAllCustomers().then(data => {
@ -772,6 +797,11 @@ const RoutesDashboard = () => {
const customMenuDate = React.forwardRef(
({ children, style, className, 'aria-labelledby': labeledBy }, ref) => {
const handleDateChange = (v) => {
setDateSelected(v);
setShowDateDropdown(false);
};
return (
<div
ref={ref}
@ -781,14 +811,8 @@ const RoutesDashboard = () => {
>
<div className="app-main-content-fields-section margin-sm dropdown-container">
<div className="me-4">
<div className="field-label">Select Date to Start</div>
<DatePicker selected={dateSelected} onChange={(v) => setDateSelected(v)} />
</div>
</div>
<div className="list row">
<div className="col-md-12">
<button className="btn btn-default btn-sm float-right" onClick={() => cleanDate()}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => FilterAndClose()}> Filter </button>
<div className="field-label">Select Date to View Report</div>
<DatePicker selected={dateSelected} onChange={handleDateChange} />
</div>
</div>
</div>
@ -866,89 +890,163 @@ const RoutesDashboard = () => {
setShowSignatureRequestLoading(false);
}
const confimHasBreakfast = (customer) => {
TransRoutesService.createBreakfastRecords({
customer_id: customer?.customer_id,
customer_name: customer?.customer_name,
has_breakfast: true,
date: moment(new Date()).format('MM/DD/YYYY'),
create_by: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name,
create_date: new Date(),
edit_history: [{
employee: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name,
date: new Date()
}]
}).then(() => {
dispatch(fetchAllBreakfastRecords());
})
}
const confirmHasLunch = (customer) => {
TransRoutesService.createLunchRecords({
customer_id: customer?.customer_id,
customer_name: customer?.customer_name,
has_lunch: true,
date: moment(new Date()).format('MM/DD/YYYY'),
create_by: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name,
create_date: new Date(),
edit_history: [{
employee: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name,
date: new Date()
}]
}).then(() => {
dispatch(fetchAllLunchRecords());
})
}
// Save template modal handlers
const openSaveTemplateModal = () => {
setShowSaveTemplateModal(true);
setTemplateName('');
};
const confirmHasSnack = (customer) => {
TransRoutesService.createSnackRecords({
customer_id: customer?.customer_id,
customer_name: customer?.customer_name,
has_snack: true,
date: moment(new Date()).format('MM/DD/YYYY'),
create_by: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name,
create_date: new Date(),
edit_history: [{
employee: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name,
date: new Date()
}]
}).then(() => {
dispatch(fetchAllSnackRecords());
})
}
const closeSaveTemplateModal = () => {
setShowSaveTemplateModal(false);
setTemplateName('');
};
const removeBreakfastRecord = (customer_id) => {
const breakfast = breakfastRecords.find(b => b.customer_id === customer_id)?.id;
TransRoutesService.deleteBreakfastRecords(breakfast).then(() => {
dispatch(fetchAllBreakfastRecords());
})
}
const removeLunchRecord = (customer_id) => {
const lunch = lunchRecords.find(b => b.customer_id === customer_id)?.id;
TransRoutesService.deleteLunchRecords(lunch).then(() => {
dispatch(fetchAllLunchRecords());
})
}
const removeSnackRecord = (customer_id) => {
const snack = snackRecords.find(b => b.customer_id === customer_id)?.id;
TransRoutesService.deleteSnackRecords(snack).then(() => {
dispatch(fetchAllSnackRecords());
})
}
const saveRoutesAsTemplate = () => {
if (!templateName || templateName.trim() === '') {
alert('Please enter a template name');
return;
}
setSavingTemplate(true);
// Get the current date string in MM/DD/YYYY format
const templateDate = getDateString(dateSelected);
// Get current user from localStorage
const currentUser = localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user'))?.name : '';
// Clean the routes data - remove all date fields and status
const cleanedRoutes = routesForShowing.map(route => {
// Clean customer list - set all date fields to null and reset dynamic fields to undefined
const cleanedCustomerList = route.route_customer_list?.map(customer => ({
...customer,
customer_enter_center_time: null,
customer_leave_center_time: null,
customer_pickup_time: null,
customer_dropoff_time: null,
// customer_estimated_pickup_time: undefined,
// customer_estimated_dropoff_time: undefined,
// customer_pickup_status: undefined,
// customer_route_status: undefined,
// customer_transfer_to_route: undefined,
// customer_address_override: undefined
})) || [];
// Return cleaned route
return {
name: route.name,
schedule_date: route.schedule_date,
vehicle: route.vehicle,
status: [], // Empty array
driver: route.driver,
type: route.type,
start_mileage: route.start_mileage,
end_mileage: route.end_mileage,
start_time: null,
end_time: null,
estimated_start_time: null,
route_customer_list: cleanedCustomerList,
checklist_result: route.checklist_result || []
};
});
// Create the template payload
const templatePayload = {
name: templateName,
template_date: templateDate,
routes: cleanedRoutes,
create_by: currentUser
};
// Call the service to create the template
DailyRoutesTemplateService.createNewDailyRoutesTemplate(templatePayload)
.then(() => {
setSavingTemplate(false);
closeSaveTemplateModal();
setSuccessMessage(`Template "${templateName}" saved successfully!`);
setTimeout(() => setSuccessMessage(undefined), 5000);
// Refresh templates list
DailyRoutesTemplateService.getAll().then((response) => {
setTemplates(response.data || []);
});
})
.catch(err => {
setSavingTemplate(false);
setErrorMessage(err.message || 'Failed to save template');
setTimeout(() => setErrorMessage(undefined), 5000);
});
};
// Apply template handler
const applyTemplate = () => {
if (!selectedTemplateId) {
alert('Please select a template');
return;
}
setApplyingTemplate(true);
// Find the selected template
const selectedTemplate = templates.find(t => t.id === selectedTemplateId);
if (!selectedTemplate) {
alert('Template not found');
setApplyingTemplate(false);
return;
}
// Get the target date in MM/DD/YYYY format
const targetDate = getDateString(dateSelected);
// Create promises for all route creations
const createPromises = selectedTemplate.routes.map(route => {
const routeData = {
...route,
schedule_date: targetDate
};
return TransRoutesService.createNewRoute(routeData);
});
// Execute all route creations
Promise.all(createPromises)
.then(() => {
setApplyingTemplate(false);
setSelectedTemplateId('');
setSuccessMessage(`Template "${selectedTemplate.name}" applied successfully to ${targetDate}!`);
setTimeout(() => setSuccessMessage(undefined), 5000);
// Refresh the routes based on current date selection
const selectedDateString = getDateString(dateSelected);
if (selectedDateString === getDateString(new Date())) {
dispatch(fetchAllRoutes());
} else {
if (dateSelected > new Date()) {
dispatch(fetchAllTomorrowRoutes({dateText: moment(dateSelected).format('MM/DD/YYYY')}));
} else {
dispatch(fetchAllHistoryRoutes({dateText: getDateString(dateSelected)}));
}
}
})
.catch(err => {
setApplyingTemplate(false);
setErrorMessage(err.message || 'Failed to apply template');
setTimeout(() => setErrorMessage(undefined), 5000);
});
};
return (
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Transportation</Breadcrumb.Item>
<Breadcrumb.Item href="/">Transportation</Breadcrumb.Item>
<Breadcrumb.Item active>
Transportation Routes
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>
All Routes
All Routes - {moment(dateSelected).format('MM/DD/YYYY (dddd)')}
</h4>
</div>
</div>
@ -958,38 +1056,65 @@ const RoutesDashboard = () => {
<Tab eventKey="allRoutesOverview" title="All Routes Overview">
{(!dateSelected || getDateString(dateSelected) === getDateString(new Date())) && <div className="app-main-content-fields-section with-function">
<button className="btn btn-primary me-2" onClick={() => directToSchedule()}><CalendarCheck size={16} className="me-2"></CalendarCheck> Schedule Tomorrow's Routes</button>
<button className="btn btn-primary me-2" onClick={() => syncCustomersInfo()}><Clock size={16} className="me-2"></Clock> {showSyncCustomersLoading? <Spinner size={12}></Spinner> : `Sync Customers Data`}</button>
<button className="btn btn-primary me-2" onClick={()=> cleanAllRoutesStatus()}><Eraser size={16} className="me-2"></Eraser > Clean Route Status</button>
<button className="btn btn-primary me-2" onClick={() => goToHistoryPage()}><ClockHistory size={16} className="me-2"></ClockHistory> View History</button>
{/* <button className="btn btn-primary me-2" onClick={() => syncCustomersInfo()}><Clock size={16} className="me-2"></Clock> {showSyncCustomersLoading? <Spinner size={12}></Spinner> : `Sync Customers Data`}</button> */}
<button className="btn btn-primary me-2" onClick={()=> handleCleanAllRoutesStatus()}><Eraser size={16} className="me-2"></Eraser > Clean Route Status</button>
{/* <button className="btn btn-primary me-2" onClick={() => goToHistoryPage()}><ClockHistory size={16} className="me-2"></ClockHistory> View History</button> */}
<button className="btn btn-primary me-2" onClick={() => copyYesterdayRoutes()}><Copy size={16} className="me-2"></Copy>{showCopyDateLoading? <Spinner size={12}></Spinner> : `Copy Yesterday Routes`}</button>
<button className="btn btn-primary" onClick={() => generateRouteReport()}><Download size={16} className="me-2"></Download>Export Route Report</button>
</div>}
{(dateSelected && dateSelected > new Date()) && <div className="app-main-content-fields-section with-function">
<button type="button" className="btn btn-primary btn-sm me-2" disabled={copyDisabled || showCopyTodayLoading} onClick={() => copyTodayRoutesOver()}><Copy size={16} className="me-2"></Copy> {showCopyTodayLoading? 'Loading...' : `Copy Today's Routes Over`}</button>
<button type="button" className="btn btn-primary btn-sm me-2" onClick={()=> validateSchedule()}><Check size={16} className="me-2"></Check> Validate and Finish Planning</button>
{/* <button type="button" className="btn btn-primary btn-sm me-2" onClick={()=> validateSchedule()}><Check size={16} className="me-2"></Check> Validate and Finish Planning</button> */}
<button type="button" className="btn btn-primary btn-sm" onClick={()=> triggerShowDeleteModal()}><Eraser size={16} className="me-2"></Eraser> Clean All Routes</button>
</div>}
{ (dateSelected <= new Date() || !dateSelected) && <div className="list row">
{
showCopyDateTargetLoading ? <><Spinner></Spinner></> : <>
<div className="col-md-12 mb-4">
{(routesInboundForShowing && routesInboundForShowing.length > 0) || (routesOutboundForShowing && routesOutboundForShowing.length > 0) ? (
<div style={{display: 'flex', alignItems: 'center', gap: '10px'}}>
<button className="btn btn-primary btn-sm" onClick={openSaveTemplateModal}>
Save Today's Routes as a Template
</button>
{/* <button className="btn btn-secondary btn-sm" onClick={() => navigate('/trans-routes/daily-templates/list')}>
View and Update Daily Route Templates
</button> */}
</div>
) : (
<div style={{display: 'flex', alignItems: 'center', gap: '10px'}}>
<select
className="form-select"
style={{width: 'auto', display: 'inline-block'}}
value={selectedTemplateId}
onChange={(e) => setSelectedTemplateId(e.target.value)}
disabled={applyingTemplate}
>
<option value="">Choose a daily template to apply to this day</option>
{templates.map(template => (
<option key={template.id} value={template.id}>
{template.name} - {template.template_date}
</option>
))}
</select>
<button
className="btn btn-primary btn-sm"
onClick={applyTemplate}
disabled={!selectedTemplateId || applyingTemplate}
>
{applyingTemplate ? <><Spinner size="sm" className="me-1" />Applying...</> : 'Submit'}
</button>
{/* <button className="btn btn-secondary btn-sm" onClick={() => navigate('/trans-routes/daily-templates/list')}>
View and Update Daily Route Templates
</button> */}
</div>
)}
</div>
<div className="col-md-12 mb-4">
<RoutesSection transRoutes={routesInboundForShowing} drivers={drivers} vehicles={vehicles} sectionName="Inbound Routes"/>
</div>
<div className="col-md-12 mb-4">
<RoutesSection transRoutes={routesOutboundForShowing} drivers={drivers} vehicles={vehicles} sectionName="Outbound Routes"/>
</div>
{(!dateSelected || getDateString(dateSelected) === getDateString(new Date())) &&<div className="list row">
<div className="col-md-4 col-sm-12 mb-4">
<BreakfastSection transRoutes={allRoutes} breakfastRecords={breakfastRecords} confimHasBreakfast={confimHasBreakfast} removeBreakfastRecord={removeBreakfastRecord} sectionName={'Breakfast Info'}/>
</div>
<div className="col-md-4 col-sm-12 mb-4">
<LunchSection transRoutes={allRoutes} lunchRecords={lunchRecords} confirmHasLunch={confirmHasLunch} removeLunchRecord={removeLunchRecord} sectionName={'Lunch Info'}/>
</div>
<div className="col-md-4 col-sm-12 mb-4">
<SnackSection transRoutes={allRoutes} snackRecords={snackRecords} confirmHasSnack={confirmHasSnack} removeSnackRecord={removeSnackRecord} sectionName={'Snack Info'}/>
</div>
<hr/>
</div>}
</>
}
@ -1009,6 +1134,45 @@ const RoutesDashboard = () => {
<span className="visually-hidden">Loading...</span>
</Spinner></div>}
{!isLoading && <>
<div className="col-md-12 mb-4">
{(tmrInboundRoutes && tmrInboundRoutes.length > 0) || (tmrOutboundRoutes && tmrOutboundRoutes.length > 0) ? (
<div style={{display: 'flex', alignItems: 'center', gap: '10px'}}>
<button className="btn btn-primary btn-sm" onClick={openSaveTemplateModal}>
Save Today's Routes as a Template
</button>
{/* <button className="btn btn-secondary btn-sm" onClick={() => navigate('/trans-routes/daily-templates/list')}>
View and Update Daily Route Templates
</button> */}
</div>
) : (
<div style={{display: 'flex', alignItems: 'center', gap: '10px'}}>
<select
className="form-select"
style={{width: 'auto', display: 'inline-block'}}
value={selectedTemplateId}
onChange={(e) => setSelectedTemplateId(e.target.value)}
disabled={applyingTemplate}
>
<option value="">Choose a daily template to apply to this day</option>
{templates.map(template => (
<option key={template.id} value={template.id}>
{template.name} - {template.template_date}
</option>
))}
</select>
<button
className="btn btn-primary btn-sm"
onClick={applyTemplate}
disabled={!selectedTemplateId || applyingTemplate}
>
{applyingTemplate ? <><Spinner size="sm" className="me-1" />Applying...</> : 'Submit'}
</button>
{/* <button className="btn btn-secondary btn-sm" onClick={() => navigate('/trans-routes/daily-templates/list')}>
View and Update Daily Route Templates
</button> */}
</div>
)}
</div>
<div className="col-md-12 mb-4">
<RoutesSection transRoutes={tmrInboundRoutes} copyList={tmrOutboundRoutes} addText="+Add Route" copyText="+Copy Route from Outbound" canAddNew={true} drivers={drivers} vehicles={vehicles} redirect={goToCreateRoute} routeType="inbound" sectionName="Inbound Routes"/>
</div>
@ -1034,6 +1198,7 @@ const RoutesDashboard = () => {
}
</Tab>
<Tab eventKey="allRoutesSignature" title="All Routes Signature">
<input className="me-2 mb-2 with-search-icon" type="text" placeholder="Search" value={keyword} onChange={(e) => setKeyword(e.currentTarget.value)} />
<table className="personnel-info-table me-4">
<thead>
<tr>
@ -1160,7 +1325,7 @@ const RoutesDashboard = () => {
autoClose={false}
>
<Dropdown.Toggle variant="primary">
<CalendarWeek size={16} className="me-2"></CalendarWeek>Select Date to View & Copy From
<CalendarWeek size={16} className="me-2"></CalendarWeek>Select Date to View OR Copy From
</Dropdown.Toggle>
<Dropdown.Menu as={customMenuOriginDate}/>
</Dropdown>
@ -1184,7 +1349,7 @@ const RoutesDashboard = () => {
}
{ currentTab === 'allRoutesSignature' && <>
<input className="me-2 with-search-icon" type="text" placeholder="Search" value={keyword} onChange={(e) => setKeyword(e.currentTarget.value)} />
<Dropdown
key={'signature-date'}
id="signature-date"
@ -1263,6 +1428,36 @@ const RoutesDashboard = () => {
</Button>
</Modal.Footer>
</Modal>
<Modal show={showSaveTemplateModal} onHide={closeSaveTemplateModal}>
<Modal.Header closeButton>
<Modal.Title>Save Routes as Template</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="mb-3">
<label className="form-label">Template Name</label>
<input
type="text"
className="form-control"
placeholder="Enter template name"
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
disabled={savingTemplate}
/>
</div>
<div className="text-muted">
<small>This will save all routes from {getDateString(dateSelected)} as a reusable template.</small>
</div>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={closeSaveTemplateModal} disabled={savingTemplate}>
Cancel
</Button>
<Button variant="primary" onClick={saveRoutesAsTemplate} disabled={savingTemplate}>
{savingTemplate ? <><Spinner size="sm" className="me-2" />Saving...</> : 'Submit'}
</Button>
</Modal.Footer>
</Modal>
</div>
</div>

View File

@ -263,9 +263,9 @@ const RoutesSchedule = () => {
<div className="col-lg-3 col-md-6 col-sm-6 col-xs-12 mb-4">
<button type="button" className="btn btn-primary btn-sm me-2 mb-4" disabled={copyDisabled || showCopyTodayLoading} onClick={() => copyTodayRoutesOver()}>{showCopyTodayLoading? 'Loading...' : `Copy Today's Routes Over`}</button>
</div>
<div className="col-lg-4 col-md-6col-sm-12 col-xs-12 mb-4">
{/* <div className="col-lg-4 col-md-6col-sm-12 col-xs-12 mb-4">
<button type="button" className="btn btn-primary btn-sm" onClick={()=> validateSchedule()}>Validate and Finish Planning</button>
</div>
</div> */}
<div className="col-lg-4 col-md-6 col-sm-12 col-xs-12 mb-4">
<button type="button" className="btn btn-primary btn-sm" onClick={()=> triggerShowDeleteModal()}>Clean All Routes</button>
</div>

View File

@ -7,7 +7,7 @@ import { CUSTOMER_TYPE, PERSONAL_ROUTE_STATUS, PICKUP_STATUS } from "../../share
import { AuthService } from "../../services";
import moment from 'moment';
const RoutesSection = ({transRoutes, copyList, sectionName, drivers, vehicles, canAddNew, addText, copyText, redirect, routeType}) => {
const RoutesSection = ({transRoutes, copyList, sectionName, drivers, vehicles, canAddNew, addText, copyText, redirect, routeType, isTemplate, templateId}) => {
const [showCopyModal, setShowCopyModal] = useState(false);
const [routesOptions, setRouteOptions] = useState([]);
const [newRoute, setNewRoute] = useState(undefined);
@ -107,7 +107,7 @@ const RoutesSection = ({transRoutes, copyList, sectionName, drivers, vehicles, c
transRoutes && (<>
<div className="all-routes-container">
{
transRoutes.map((r) => Object.assign({}, r, {vehicleNumber: vehicles?.find((v) => r.vehicle === v.id)?.vehicle_number || 0})).sort((a, b) => a?.vehicleNumber - b?.vehicleNumber).map((route, index) => <div key={route.id} className="route-card-container"><RouteCard routeIndex={index} transRoute={route} drivers={drivers} vehicles={vehicles}/></div>)
transRoutes.map((r) => Object.assign({}, r, {vehicleNumber: vehicles?.find((v) => r.vehicle === v.id)?.vehicle_number || 0})).sort((a, b) => a?.vehicleNumber - b?.vehicleNumber).map((route, index) => <div key={route.id} className="route-card-container"><RouteCard routeIndex={index} transRoute={route} drivers={drivers} vehicles={vehicles} isTemplate={isTemplate} templateId={templateId}/></div>)
}
</div>
</>)

View File

@ -29,8 +29,10 @@ const SnackSection = ({transRoutes, snackRecords, sectionName, confirmHasSnack,
}
useEffect(() => {
const routeCustomers = TransRoutesService.getAllInCenterCustomersFromRoutesForSnack(transRoutes, snackRecords);
setCustomers(routeCustomers);
if (transRoutes && transRoutes.length > 0) {
const routeCustomers = TransRoutesService.getAllInCenterCustomersFromRoutesForSnack(transRoutes, snackRecords || []);
setCustomers(routeCustomers);
}
}, [snackRecords, transRoutes]);
@ -51,20 +53,20 @@ const SnackSection = ({transRoutes, snackRecords, sectionName, confirmHasSnack,
<th>Change Snack Status</th>
</tr>
</thead>
<tbody>
{
snackRecords?.length >0 && customers?.map((customer) => (
<tr className={customer?.has_snack ? 'light-green' : 'red'}>
<td>{customer?.customer_name}</td>
<td>{customer?.has_snack ? 'Yes': 'No'}</td>
<td>
{!customer?.has_snack && <button className="btn btn-link btn-sm" onClick={() => confirmHasSnack(customer)}>Confirm Customer Has snack</button>}
{customer?.has_snack && <button className="btn btn-link btn-sm" onClick={() => removeSnackRecord(customer?.customer_id)}>Mark Customer NOT have snack</button>}
</td>
</tr>
))
}
</tbody>
<tbody>
{
customers?.map((customer) => (
<tr key={customer?.customer_id} className={customer?.has_snack ? 'light-green' : 'red'}>
<td>{customer?.customer_name}</td>
<td>{customer?.has_snack ? 'Yes': 'No'}</td>
<td>
{!customer?.has_snack && <button className="btn btn-link btn-sm" onClick={() => confirmHasSnack(customer)}>Confirm Customer Has snack</button>}
{customer?.has_snack && <button className="btn btn-link btn-sm" onClick={() => removeSnackRecord(customer?.customer_id)}>Mark Customer NOT have snack</button>}
</td>
</tr>
))
}
</tbody>
</table>
</div>
</div>

View File

@ -1,60 +1,100 @@
import React, {useEffect, useState} from "react";
import { useSelector,useDispatch } from "react-redux";
import { useSelector, useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { vehicleSlice, selectVehicleError } from "./../../store";
import { AuthService, VehicleService } from "../../services";
import { AuthService, VehicleService, DriverService } from "../../services";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab } from "react-bootstrap";
import { Upload } from "react-bootstrap-icons";
import DatePicker from "react-datepicker";
import Select from 'react-select';
import moment from 'moment';
import {
SEATING_CAPACITY_OPTIONS,
FUEL_TYPE, FUEL_TYPE_TEXT,
VEHICLE_TITLE, VEHICLE_TITLE_TEXT,
LIFT_EQUIPPED, LIFT_EQUIPPED_TEXT,
REPAIR_PART_NAME, REPAIR_PART_NAME_TEXT,
QUANTITY_OPTIONS
} from "../../shared";
const CreateVehicle = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const { createVehicle } = vehicleSlice.actions;
const error = useSelector(selectVehicleError);
// Basic Information
const [vehicleNumber, setVehicleNumber] = useState('');
const [responsibleDriver, setResponsibleDriver] = useState(null);
const [capacity, setCapacity] = useState('');
const [mileage, setMileage] = useState('');
const [make, setMake] = useState('');
const [vehicleModel, setVehicleModel] = useState('');
const [year, setYear] = useState('');
const [vin, setVin] = useState('');
const [tag, setTag] = useState('');
const [gps, setGps] = useState('');
const [ezpass, setEzpass] = useState('');
const [hasLiftEquip, setHasLiftEquip] = useState('');
const [fuelType, setFuelType] = useState('');
const [title, setTitle] = useState('');
const [titleOther, setTitleOther] = useState('');
// Compliance & Deadlines
const [insuranceStartDate, setInsuranceStartDate] = useState(null);
const [vehicleRegistrationDate, setVehicleRegistrationDate] = useState(null);
// Check List
const [checklist, setChecklist] = useState(['']);
// Additional Information
const [note, setNote] = useState('');
// Drivers list
const [drivers, setDrivers] = useState([]);
// Documents
const [selectedMonthlyFile, setSelectedMonthlyFile] = useState();
const [selectedYearlyFile, setSelectedYearlyFile] = useState();
const [monthlyInspectionDate, setMonthlyInspectionDate] = useState(null);
const [yearlyInspectionDate, setYearlyInspectionDate] = useState(null);
// Repair & Maintenance Record
const [repairPartName, setRepairPartName] = useState('');
const [repairReplacementDate, setRepairReplacementDate] = useState(null);
const [repairMileage, setRepairMileage] = useState('');
const [repairQuantity, setRepairQuantity] = useState('');
const [repairCost, setRepairCost] = useState('');
const [repairLocation, setRepairLocation] = useState('');
const [repairReceiptFile, setRepairReceiptFile] = useState(null);
const [repairNextReminder, setRepairNextReminder] = useState('');
useEffect(() => {
if (!AuthService.canAddOrEditVechiles()) {
window.alert('You haven\'t login yet OR this user does not have access to this page. Please change an Dispatcher or admin account to login.')
AuthService.logout();
navigate(`/login`);
}
// Fetch drivers list
DriverService.getAllActiveDrivers('driver', 'active').then(data => {
setDrivers(data.data || []);
}).catch(() => {
setDrivers([]);
});
}, []);
const { createVehicle } = vehicleSlice.actions;
const [vehicleNumber, setVehicleNumber] = useState();
const [tag, setTag] = useState('');
const [make, setMake] = useState('');
const [vehicleModel, setVehicleModel] = useState('');
const [year, setYear] = useState('');
const [ezpass, setEzpass] = useState('');
const [gps, setGps] = useState('');
const [mileage, setMileage] = useState();
const [capacity, setCapacity] = useState();
const [checklist, setChecklist] = useState(['']);
const [hasLiftEquip, setHasLiftEquip] = useState(undefined);
const [insuranceExpireOn, setInsuranceExpireOn] = useState(undefined);
const [titleRegistrationOn, setTitleRegistrationOn] = useState(undefined);
const [emissionTestOn, setEmissionTestOn] = useState(undefined);
const [oilChangeMileage, setOilChangeMileage] = useState(undefined);
const [oilChangeDate, setOilChangeDate] = useState(undefined);
const [vin, setVin] = useState('');
const [note, setNote] = useState('');
// const [selectedMothlyFile, setSelectedMonthlyFile] = useState();
// const [selectedYearlyFile, setSelectedYearlyFile] = useState();
// const [monthlyInspectionDate, setMonthlyInspectionDate] = useState();
// const [yearlyInspectionDate, setYearlyInspectionDate] = useState();
const error = useSelector(selectVehicleError);
const redirectTo = () => {
const params = new URLSearchParams(window.location.search);
const redirect = params.get('redirect');
if (redirect === 'schedule') {
navigate(`/trans-routes/schedule`);
} else {
// navigate(`/trans-routes/dashboard`);
redirectToList();
}
const redirect = params.get('redirect');
if (redirect === 'schedule') {
navigate(`/trans-routes/schedule`);
} else {
redirectToList();
}
}
const redirectToList = () => {
navigate('/vehicles/list')
navigate('/vehicles/list')
}
const addItemToArray = () => {
@ -62,187 +102,411 @@ const CreateVehicle = () => {
setChecklist(arr);
}
const saveVechile = () => {
const data = {
vehicle_number: vehicleNumber,
tag,
ezpass,
gps_tag: gps,
mileage,
capacity,
year,
make,
vehicle_model: vehicleModel,
status: 'active',
checklist,
note,
vin,
has_lift_equip: hasLiftEquip === 'true',
insurance_expire_on: moment(insuranceExpireOn).format('MM/DD/YYYY'),
title_registration_on: moment(titleRegistrationOn).format('MM/DD/YYYY'),
emission_test_on: moment(emissionTestOn).format('MM/DD/YYYY'),
oil_change_date: moment(oilChangeDate).format('MM/DD/YYYY')
};
dispatch(createVehicle({data, redirectFun: redirectTo}));
}
const formatDateForBackend = (date) => {
if (!date) return '';
return moment(date).format('MM/DD/YYYY');
}
// const saveDocument = () => {
// if (selectedMothlyFile && monthlyInspectionDate) {
// VehicleService.uploadVechileFile(formData, currentVechile.id, currentVehchile.vehicle_number, 'monthlyInspection', monthlyInspectionDate);
// }
// if (selectedYearlyFile && yearlyInspectionDate) {
// VehicleService.uploadVechileFile(formData, currentVechile.id, currentVehchile.vehicle_number, 'yearlyInspection', yearlyInspectionDate);
// }
// }
const validateVehicle = () => {
const errors = [];
// Required fields validation
if (!vehicleNumber || vehicleNumber.trim() === '') {
errors.push('Vehicle Number');
}
if (!tag || tag.trim() === '') {
errors.push('License Plate');
}
if (errors.length > 0) {
window.alert(`Please fill in the following required fields:\n${errors.join('\n')}`);
return false;
}
return true;
};
const saveVehicle = () => {
if (!validateVehicle()) {
return;
}
const data = {
// Basic Information
vehicle_number: vehicleNumber,
responsible_driver: responsibleDriver?.label || '',
responsible_driver_id: responsibleDriver?.value || '',
capacity: parseInt(capacity) || 0,
mileage: parseInt(mileage) || 0,
make,
vehicle_model: vehicleModel,
year,
vin,
tag,
gps_tag: gps,
ezpass,
has_lift_equip: hasLiftEquip === 'true',
fuel_type: fuelType,
title,
title_other: titleOther,
// Compliance & Deadlines
insurance_start_date: formatDateForBackend(insuranceStartDate),
vehicle_registration_date: formatDateForBackend(vehicleRegistrationDate),
// Check List
checklist,
// Additional Information
note,
// System fields
status: 'active'
};
dispatch(createVehicle({data, redirectFun: redirectTo}));
}
// Custom styles for react-select
const selectStyles = {
control: (baseStyles) => ({
...baseStyles,
minWidth: '200px',
borderRadius: '8px'
}),
indicatorSeparator: () => ({
display: 'none'
}),
dropdownIndicator: (baseStyles) => ({
...baseStyles,
paddingRight: '8px',
color: '#333',
'&:hover': {
color: '#000',
},
}),
valueContainer: (baseStyles) => ({
...baseStyles,
height: '43px',
padding: '0 8px',
}),
input: (baseStyles) => ({
...baseStyles,
margin: '0px',
padding: '0px',
height: '30px',
width: '290px'
}),
singleValue: (baseStyles) => ({
...baseStyles,
margin: '0px',
}),
};
return (
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Transportation</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/trans-routes/dashboard">Transportation</Breadcrumb.Item>
<Breadcrumb.Item href="/vehicles/list">
Vehicles Information
</Breadcrumb.Item>
<Breadcrumb.Item active>
Create New Vehicle
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>Create New Vechile <button className="btn btn-link btn-sm" onClick={() => {redirectTo()}}>Back</button></h4>
<div className="col-md-12 text-primary">
<h4>Create New Vehicle <button className="btn btn-link btn-sm" onClick={() => {redirectTo()}}>Back</button></h4>
</div>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="basicInfo" id="customers-tab">
<Tab eventKey="basicInfo" title="Basic Information">
<h6 className="text-primary">Vehicle Information</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Vehicle Number
<span className="required">*</span>
</div>
<input type="number" placeholder="e.g.,1" value={vehicleNumber || ''} onChange={e => setVehicleNumber(e.target.value)}/>
</div>
<div className="me-4"><div className="field-label">Seating Capacity</div><input type="number" value={capacity || ''} placeholder="e.g.,12" onChange={e => setCapacity(e.target.value)}/></div>
<div className="me-4"><div className="field-label">Mileage</div><input type="number" value={mileage || ''} placeholder="e.g.,48000" onChange={e => setMileage(e.target.value)}/></div>
<div className="me-4"><div className="field-label">Make</div><input type="text" value={make || ''} placeholder="e.g.,Ford" onChange={e => setMake(e.target.value)}/></div>
<div className="me-4"><div className="field-label">Model</div><input type="text" value={vehicleModel || ''} placeholder="e.g.,T350" onChange={e => setVehicleModel(e.target.value)}/></div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4"><div className="field-label">Year</div><input type="text" value={year || ''} placeholder="e.g.,2016" onChange={e => setYear(e.target.value)}/></div>
<div className="me-4"><div className="field-label">Vin Number</div><input type="text" value={vin || ''} placeholder="e.g, 1FBAX2CM9KA34959" onChange={e => setVin(e.target.value)}/></div>
<div className="me-4"><div className="field-label">Licence Plate</div><input type="text" value={tag || ''} placeholder="e.g.,91579HT" onChange={e => setTag(e.target.value)}/></div>
<div className="me-4"><div className="field-label">GPS ID</div><input type="text" value={gps || ''} placeholder="e.g.,609671" onChange={e => setGps(e.target.value)}/></div>
<div className="me-4"><div className="field-label">EZPass</div><input type="text" value={ezpass || ''} placeholder="e.g.,NY12345" onChange={e => setEzpass(e.target.value)}/></div>
<div className="me-4">
<div className="app-main-content-list-container form-page">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="basicInfo" id="vehicles-tab">
<Tab eventKey="basicInfo" title="Basic Information">
<h6 className="text-primary">Basic Information</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Vehicle Number <span className="required">*</span></div>
<input type="text" placeholder="e.g., 101" value={vehicleNumber} onChange={e => setVehicleNumber(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Responsible Driver</div>
<Select
value={responsibleDriver}
onChange={setResponsibleDriver}
options={[{value: '', label: ''}, ...drivers.map(driver => ({
value: driver?.id || '',
label: driver?.name || '',
}))]}
styles={selectStyles}
placeholder="e.g., John Smith"
isClearable
/>
</div>
<div className="me-4">
<div className="field-label">Seating Capacity</div>
<select value={capacity} onChange={e => setCapacity(e.target.value)}>
<option value="">Select...</option>
{SEATING_CAPACITY_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
<div className="me-4">
<div className="field-label">Mileage</div>
<input type="number" placeholder="e.g., 48000" value={mileage} onChange={e => setMileage(e.target.value)}/>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Make</div>
<input type="text" placeholder="e.g., Ford" value={make} onChange={e => setMake(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Model</div>
<input type="text" placeholder="e.g., T350" value={vehicleModel} onChange={e => setVehicleModel(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Year</div>
<input type="text" placeholder="e.g., 2018" value={year} onChange={e => setYear(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">VIN Number</div>
<input type="text" placeholder="e.g., 1FBAX2CM9KKA34959" value={vin} onChange={e => setVin(e.target.value)}/>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">License Plate <span className="required">*</span></div>
<input type="text" placeholder="e.g., 91579HT" value={tag} onChange={e => setTag(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">GPS ID</div>
<input type="text" placeholder="e.g., 609671" value={gps} onChange={e => setGps(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">E-ZPass</div>
<input type="text" placeholder="e.g., NY12345" value={ezpass} onChange={e => setEzpass(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Lift Equipped</div>
<select value={hasLiftEquip} onChange={e => setHasLiftEquip(e.target.value)}>
<option value=""></option>
<option value="true">Yes</option>
<option value="false">No</option>
</select>
<select value={hasLiftEquip} onChange={e => setHasLiftEquip(e.target.value)}>
<option value="">Select...</option>
{Object.keys(LIFT_EQUIPPED).map(key => (
<option key={key} value={LIFT_EQUIPPED[key]}>{LIFT_EQUIPPED_TEXT[LIFT_EQUIPPED[key]]}</option>
))}
</select>
</div>
</div>
<h6 className="text-primary">Vehicle Maintenance & Compliance</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Last Oil Change Date <span className="field-blurb float-right">1-month due cycle </span></div>
<DatePicker selected={oilChangeDate} onChange={(v) => setOilChangeDate(v)} />
</div>
<div className="me-4">
<div className="field-label">Last Emissions Inspection Date <span className="field-blurb float-right">1-year due cycle </span></div>
<DatePicker selected={emissionTestOn} onChange={(v) => setEmissionTestOn(v)} />
</div>
<div className="me-4">
<div className="field-label">Insurance Expiration Date <span className="field-blurb float-right">1-year due cycle </span></div>
<DatePicker selected={insuranceExpireOn} onChange={(v) => setInsuranceExpireOn(v)} />
</div>
<div className="me-4">
<div className="field-label">Title Registration Date <span className="field-blurb float-right">1-year due cycle </span></div>
<DatePicker selected={titleRegistrationOn} onChange={(v) => setTitleRegistrationOn(v)} />
</div>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Fuel Type</div>
<select value={fuelType} onChange={e => setFuelType(e.target.value)}>
<option value="">Select...</option>
{Object.keys(FUEL_TYPE).map(key => (
<option key={key} value={FUEL_TYPE[key]}>{FUEL_TYPE_TEXT[FUEL_TYPE[key]]}</option>
))}
</select>
</div>
<div className="me-4">
<div className="field-label">Title</div>
<select value={title} onChange={e => setTitle(e.target.value)}>
<option value="">Select...</option>
{Object.keys(VEHICLE_TITLE).map(key => (
<option key={key} value={VEHICLE_TITLE[key]}>{VEHICLE_TITLE_TEXT[VEHICLE_TITLE[key]]}</option>
))}
</select>
</div>
{title === 'other' && (
<div className="me-4">
<div className="field-label">Title (Other)</div>
<input type="text" placeholder="Please specify..." value={titleOther} onChange={e => setTitleOther(e.target.value)}/>
</div>
)}
</div>
<h6 className="text-primary">Check List</h6>
<div className="app-main-content-fields-section column">
{checklist.map((item, index) => (
<div className="mb-4" key={index}>
<input type="text" placeholder="e.g., Tire pressure" value={item} onChange={(e) => setChecklist([...checklist].map((a, index1) => {if (index1 === index) {return e.target.value;} return a;}))}/>
<button className="btn btn-link btn-sm" onClick={(e) => setChecklist([...checklist].filter((value, index1) => index1 != index))}>Remove</button>
</div>
))}
<button className="btn btn-link" onClick={() => addItemToArray()}>+Add New Item</button>
</div>
<h6 className="text-primary">Check List</h6>
<div className="app-main-content-fields-section column">
{checklist.map((item, index) => (<div className="mb-4" key={index}><input type="text" value={item} onChange={(e) => setChecklist([...checklist].map((a, index1) => {if (index1 === index) {return e.target.value;} return a;}))}/>
<button className="btn btn-link btn-sm" onClick={(e) => setChecklist([...checklist].filter((value, index1) => index1 != index))}>Remove</button>
</div>))}
<button className="btn btn-link" onClick={() => addItemToArray()}>+Add New Item</button>
</div>
<h6 className="text-primary">Additional Information</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Notes and Attachments</div>
<textarea placeholder="e.g., Vehicle assigned to Route A" value={note} onChange={e => setNote(e.target.value)} rows={4} style={{width: '400px'}}/>
</div>
</div>
<h6 className="text-primary">Additional Information</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Note</div>
<textarea placeholder="Any Extra Details" value={note || ''} onChange={e => setNote(e.target.value)}/>
</div>
</div>
{error && <div className="col-md-12 mb-4 alert alert-danger" role="alert">
{error}
</div>}
<div className="list row mb-5">
<div className="col-md-12 col-sm-12 col-xs-12">
<button className="btn btn-default btn-sm float-right" onClick={() => redirectTo()}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => saveVehicle()}> Save </button>
</div>
</div>
</Tab>
{error && <div className="col-md-12 mb-4 alert alert-danger" role="alert">
{error}
</div>}
<div className="col-md-12 col-sm-12 col-xs-12">
<button className="btn btn-default btn-sm float-right" onClick={() => redirectTo()}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => saveVechile()}> Save </button>
</div>
</Tab>
{/* <Tab eventKey="documents" title="Documents">
<h6 className="text-primary">Yearly Vehicle Inspection</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Vehicle Inspection Date<span className="required">*</span></div>
<DatePicker selected={yearlyInspectionDate} onChange={(v) => setYearlyInspectionDate(v)} />
</div>
<div className="me-4">
<div className="field-label">Yearly Vehicle Inspection Sheet<span className="required">*</span></div>
<Tab eventKey="complianceDeadlines" title="Compliance & Deadlines">
<h6 className="text-primary">Compliance & Deadlines</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Insurance Expiration Date</div>
<DatePicker
selected={insuranceStartDate}
onChange={(date) => setInsuranceStartDate(date)}
dateFormat="MM/dd/yyyy"
placeholderText="e.g., 03/01/2024"
className="form-control"
/>
</div>
<div className="me-4">
<div className="field-label">Vehicle Registration Date</div>
<DatePicker
selected={vehicleRegistrationDate}
onChange={(date) => setVehicleRegistrationDate(date)}
dateFormat="MM/dd/yyyy"
placeholderText="e.g., 03/01/2024"
className="form-control"
/>
</div>
</div>
{error && <div className="col-md-12 mb-4 alert alert-danger" role="alert">
{error}
</div>}
<div className="list row mb-5">
<div className="col-md-12 col-sm-12 col-xs-12">
<button className="btn btn-default btn-sm float-right" onClick={() => redirectTo()}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => saveVehicle()}> Save </button>
</div>
</div>
</Tab>
<Tab eventKey="documents" title="Documents & Records">
<h6 className="text-primary">Yearly Inspection</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Yearly Inspection Date</div>
<DatePicker
selected={yearlyInspectionDate}
onChange={(date) => setYearlyInspectionDate(date)}
dateFormat="MM/dd/yyyy"
placeholderText="e.g., 03/01/2024"
className="form-control"
/>
</div>
<div className="me-4">
<div className="field-label">Yearly Inspection File</div>
<label className="custom-file-upload">
<Upload width={20} color={"#fff"} className="me-2"></Upload> Upload Files
<input
type="file"
onChange={(e) => setSelectedYearlyFile(e.target.files[0])}
/>
<Upload width={20} color={"#fff"} className="me-2"></Upload> Upload
<input type="file" onChange={(e) => setSelectedYearlyFile(e.target.files[0])}/>
</label>
<div className="file-name">{ selectedYearlyFile && selectedYearlyFile?.name }</div>
<div className="file-name">{selectedYearlyFile?.name}</div>
</div>
</div>
</div>
<h6 className="text-primary">Monthly Vehicle Inspection</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Vehicle Inspection Date <span className="required">*</span></div>
<DatePicker selected={monthlyInspectionDate} onChange={(v) => setMonthlyInspectionDate(v)} />
</div>
<div className="me-4">
<div className="field-label">Monthly Vehicle Inspection Sheet<span className="required">*</span></div>
<label className="custom-file-upload">
<Upload width={20} color={"#fff"} className="me-2"></Upload> Upload Files
<input
type="file"
onChange={(e) => setSelectedMonthlyFile(e.target.files[0])}
/>
</label>
<div className="file-name">{ selectedMothlyFile && selectedMothlyFile?.name }</div>
<h6 className="text-primary">Monthly Inspection</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Monthly Inspection Date</div>
<DatePicker
selected={monthlyInspectionDate}
onChange={(date) => setMonthlyInspectionDate(date)}
dateFormat="MM/dd/yyyy"
placeholderText="e.g., 03/01/2024"
className="form-control"
/>
</div>
</div>
<div className="col-md-12 col-sm-12 col-xs-12">
<button className="btn btn-default btn-sm float-right" onClick={() => redirectTo()}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => saveDocuments()}> Save </button>
</div>
</Tab>
<Tab eventKey="Repair Records" title="Repair Records">
Coming soon...
</Tab> */}
</Tabs>
</div>
</div>
<div className="me-4">
<div className="field-label">Monthly Inspection File</div>
<label className="custom-file-upload">
<Upload width={20} color={"#fff"} className="me-2"></Upload> Upload
<input type="file" onChange={(e) => setSelectedMonthlyFile(e.target.files[0])}/>
</label>
<div className="file-name">{selectedMonthlyFile?.name}</div>
</div>
</div>
<h6 className="text-primary">Repair & Maintenance Record</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Part Name</div>
<select value={repairPartName} onChange={e => setRepairPartName(e.target.value)}>
<option value="">Select...</option>
{Object.keys(REPAIR_PART_NAME).map(key => (
<option key={key} value={REPAIR_PART_NAME[key]}>{REPAIR_PART_NAME_TEXT[REPAIR_PART_NAME[key]]}</option>
))}
</select>
</div>
<div className="me-4">
<div className="field-label">Replacement Date</div>
<DatePicker
selected={repairReplacementDate}
onChange={(date) => setRepairReplacementDate(date)}
dateFormat="MM/dd/yyyy"
placeholderText="e.g., 03/01/2024"
className="form-control"
/>
</div>
<div className="me-4">
<div className="field-label">Mileage at Replacement</div>
<input type="text" placeholder="e.g., 48,000" value={repairMileage} onChange={e => setRepairMileage(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Quantity</div>
<select value={repairQuantity} onChange={e => setRepairQuantity(e.target.value)}>
<option value="">Select...</option>
{QUANTITY_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Cost</div>
<input type="text" placeholder="e.g., $250.00" value={repairCost} onChange={e => setRepairCost(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Location</div>
<input type="text" placeholder="e.g., Rockville Auto Center" value={repairLocation} onChange={e => setRepairLocation(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Receipt Upload</div>
<label className="custom-file-upload">
<Upload width={20} color={"#fff"} className="me-2"></Upload> Upload
<input type="file" onChange={(e) => setRepairReceiptFile(e.target.files[0])}/>
</label>
<div className="file-name">{repairReceiptFile?.name}</div>
</div>
<div className="me-4">
<div className="field-label">Next Replacement Reminder</div>
<input type="text" placeholder="e.g., 78,000" value={repairNextReminder} onChange={e => setRepairNextReminder(e.target.value)}/>
</div>
</div>
{error && <div className="col-md-12 mb-4 alert alert-danger" role="alert">
{error}
</div>}
<div className="list row mb-5">
<div className="col-md-12 col-sm-12 col-xs-12">
<button className="btn btn-default btn-sm float-right" onClick={() => redirectTo()}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => saveVehicle()}> Save </button>
</div>
</div>
</Tab>
</Tabs>
</div>
</div>
</>
);
};
export default CreateVehicle;
export default CreateVehicle;

View File

@ -1,49 +1,81 @@
import React, {useEffect, useState} from "react";
import { useSelector,useDispatch } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { vehicleSlice, selectVehicleError } from "./../../store";
import { AuthService, VehicleRepairService, VehicleService } from "../../services";
import { AuthService, VehicleRepairService, VehicleService, DriverService } from "../../services";
import { Archive, Upload } from "react-bootstrap-icons";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab } from "react-bootstrap";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab, Modal, Button } from "react-bootstrap";
import DatePicker from "react-datepicker";
import Select from 'react-select';
import moment from 'moment';
import {
SEATING_CAPACITY_OPTIONS,
FUEL_TYPE, FUEL_TYPE_TEXT,
VEHICLE_TITLE, VEHICLE_TITLE_TEXT,
LIFT_EQUIPPED, LIFT_EQUIPPED_TEXT,
REPAIR_PART_NAME, REPAIR_PART_NAME_TEXT,
QUANTITY_OPTIONS
} from "../../shared";
const UpdateVehicle = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const params = useParams();
const [searchParams] = useSearchParams();
const [activeTab, setActiveTab] = useState(searchParams.get('tab') || 'basicInfo');
const vehicles = useSelector((state) => state.vehicles && state.vehicles.vehicles);
const currentVehicle = vehicles.find(item => item.id === params.id ) || undefined;
const { updateVehicle, deleteVehicle, fetchAllVehicles } = vehicleSlice.actions;
const [vehicleNumber, setVehicleNumber] = useState();
const [tag, setTag] = useState('');
const [make, setMake] = useState('');
const [vehicleModel, setVehicleModel] = useState('');
const [year, setYear] = useState('');
const [ezpass, setEzpass] = useState('');
const [gps, setGps] = useState('');
const [mileage, setMileage] = useState();
const [capacity, setCapacity] = useState();
const [checklist, setChecklist] = useState(['']);
const [hasLiftEquip, setHasLiftEquip] = useState(undefined);
const [insuranceExpireOn, setInsuranceExpireOn] = useState(undefined);
const [titleRegistrationOn, setTitleRegistrationOn] = useState(undefined);
const [emissionTestOn, setEmissionTestOn] = useState(undefined);
const [oilChangeMileage, setOilChangeMileage] = useState(undefined);
const [oilChangeDate, setOilChangeDate] = useState(undefined);
const [vin, setVin] = useState('');
const [note, setNote] = useState('');
const [selectedMothlyFile, setSelectedMonthlyFile] = useState();
const [selectedYearlyFile, setSelectedYearlyFile] = useState();
const [monthlyInspectionDate, setMonthlyInspectionDate] = useState();
const [yearlyInspectionDate, setYearlyInspectionDate] = useState();
const [repairDate, setRepairDate] = useState();
const [repairDescription, setRepairDescription] = useState('');
const [repairPrice, setRepairPrice] = useState('');
const [repairLocation, setRepairLocation] = useState('');
const error = useSelector(selectVehicleError);
const [selectedRepairFile, setSelectedRepairFile] = useState();
// Basic Information
const [vehicleNumber, setVehicleNumber] = useState('');
const [responsibleDriver, setResponsibleDriver] = useState(null);
const [capacity, setCapacity] = useState('');
const [mileage, setMileage] = useState('');
const [make, setMake] = useState('');
const [vehicleModel, setVehicleModel] = useState('');
const [year, setYear] = useState('');
const [vin, setVin] = useState('');
const [tag, setTag] = useState('');
const [gps, setGps] = useState('');
const [ezpass, setEzpass] = useState('');
const [hasLiftEquip, setHasLiftEquip] = useState('');
const [fuelType, setFuelType] = useState('');
const [title, setTitle] = useState('');
const [titleOther, setTitleOther] = useState('');
// Compliance & Deadlines
const [insuranceStartDate, setInsuranceStartDate] = useState(null);
const [vehicleRegistrationDate, setVehicleRegistrationDate] = useState(null);
// Check List
const [checklist, setChecklist] = useState(['']);
// Additional Information
const [note, setNote] = useState('');
// Drivers list
const [drivers, setDrivers] = useState([]);
// Documents
const [selectedMonthlyFile, setSelectedMonthlyFile] = useState();
const [selectedYearlyFile, setSelectedYearlyFile] = useState();
const [monthlyInspectionDate, setMonthlyInspectionDate] = useState(null);
const [yearlyInspectionDate, setYearlyInspectionDate] = useState(null);
// Repair & Maintenance Record
const [repairPartName, setRepairPartName] = useState('');
const [repairReplacementDate, setRepairReplacementDate] = useState(null);
const [repairMileage, setRepairMileage] = useState('');
const [repairQuantity, setRepairQuantity] = useState('');
const [repairCost, setRepairCost] = useState('');
const [repairLocation, setRepairLocation] = useState('');
const [repairReceiptFile, setRepairReceiptFile] = useState(null);
const [repairNextReminder, setRepairNextReminder] = useState('');
// Modal
const [showDeleteModal, setShowDeleteModal] = useState(false);
useEffect(() => {
if (!AuthService.canAddOrEditVechiles()) {
@ -54,43 +86,66 @@ const UpdateVehicle = () => {
if (!currentVehicle) {
dispatch(fetchAllVehicles());
}
// Fetch drivers list
DriverService.getAllActiveDrivers('driver', 'active').then(data => {
setDrivers(data.data || []);
}).catch(() => {
setDrivers([]);
});
}, []);
useEffect(() => {
if (currentVehicle) {
setVehicleNumber(currentVehicle.vehicle_number);
setTag(currentVehicle.tag);
setMake(currentVehicle.make);
setVehicleModel(currentVehicle.vehicle_model);
setYear(currentVehicle.year);
setGps(currentVehicle.gps_tag);
setEzpass(currentVehicle.ezpass);
setMileage(currentVehicle.mileage);
setCapacity(currentVehicle.capacity);
setChecklist(currentVehicle.checklist);
setHasLiftEquip(currentVehicle.has_lift_equip === true ? 'true': (currentVehicle?.has_lift_equip === false ? 'false' : undefined) );
setNote(currentVehicle?.note);
setVin(currentVehicle?.vin);
setInsuranceExpireOn(currentVehicle.insurance_expire_on && VehicleService.convertToDate(currentVehicle.insurance_expire_on));
setTitleRegistrationOn(currentVehicle?.title_registration_on && VehicleService.convertToDate(currentVehicle?.title_registration_on));
setEmissionTestOn(currentVehicle.title_registration_on && VehicleService.convertToDate(currentVehicle.title_registration_on));
setOilChangeDate(currentVehicle.oil_change_date && VehicleService.convertToDate(currentVehicle.oil_change_date));
// Basic Information
setVehicleNumber(currentVehicle.vehicle_number || '');
setResponsibleDriver(currentVehicle.responsible_driver ? {
value: currentVehicle.responsible_driver_id || '',
label: currentVehicle.responsible_driver || ''
} : null);
setCapacity(currentVehicle.capacity?.toString() || '');
setMileage(currentVehicle.mileage?.toString() || '');
setMake(currentVehicle.make || '');
setVehicleModel(currentVehicle.vehicle_model || '');
setYear(currentVehicle.year || '');
setVin(currentVehicle.vin || '');
setTag(currentVehicle.tag || '');
setGps(currentVehicle.gps_tag || '');
setEzpass(currentVehicle.ezpass || '');
setHasLiftEquip(currentVehicle.has_lift_equip === true ? 'true' : (currentVehicle.has_lift_equip === false ? 'false' : ''));
setFuelType(currentVehicle.fuel_type || '');
setTitle(currentVehicle.title || '');
setTitleOther(currentVehicle.title_other || '');
// Compliance & Deadlines
setInsuranceStartDate(currentVehicle.insurance_start_date ? VehicleService.convertToDate(currentVehicle.insurance_start_date) :
(currentVehicle.insurance_expire_on ? VehicleService.convertToDate(currentVehicle.insurance_expire_on) : null));
setVehicleRegistrationDate(currentVehicle.vehicle_registration_date ? VehicleService.convertToDate(currentVehicle.vehicle_registration_date) :
(currentVehicle.title_registration_on ? VehicleService.convertToDate(currentVehicle.title_registration_on) : null));
// Check List
setChecklist(currentVehicle.checklist || ['']);
// Additional Information
setNote(currentVehicle.note || '');
}
}, [currentVehicle])
}, [currentVehicle]);
const redirectTo = () => {
const params = new URLSearchParams(window.location.search);
const redirect = params.get('redirect');
if (redirect === 'schedule') {
navigate(`/trans-routes/schedule`);
} else {
const redirect = params.get('redirect');
if (redirect === 'schedule') {
navigate(`/trans-routes/schedule`);
} else {
if (redirect === 'list') {
navigate(`/vehicles/list`)
navigate(`/vehicles/list`);
} else {
navigate(`/trans-routes/dashboard`);
}
}
}
}
const redirectToView = () => {
navigate(`/vehicles/${params.id}`);
}
const addItemToArray = () => {
@ -98,85 +153,172 @@ const UpdateVehicle = () => {
setChecklist(arr);
}
const saveVechile = () => {
const data = {
vehicle_number: vehicleNumber,
tag,
ezpass,
gps_tag: gps,
mileage,
capacity,
year,
make,
vehicle_model: vehicleModel,
status: 'active',
checklist,
const formatDateForBackend = (date) => {
if (!date) return '';
return moment(date).format('MM/DD/YYYY');
}
const buildVehicleData = () => {
return {
// Basic Information
vehicle_number: vehicleNumber,
responsible_driver: responsibleDriver?.label || '',
responsible_driver_id: responsibleDriver?.value || '',
capacity: parseInt(capacity) || 0,
mileage: parseInt(mileage) || 0,
make,
vehicle_model: vehicleModel,
year,
vin,
tag,
gps_tag: gps,
ezpass,
has_lift_equip: hasLiftEquip === 'true',
fuel_type: fuelType,
title,
title_other: titleOther,
// Compliance & Deadlines
insurance_start_date: formatDateForBackend(insuranceStartDate),
vehicle_registration_date: formatDateForBackend(vehicleRegistrationDate),
// Legacy fields for backward compatibility
insurance_expire_on: formatDateForBackend(insuranceStartDate),
title_registration_on: formatDateForBackend(vehicleRegistrationDate),
// Check List
checklist,
// Additional Information
note,
vin,
has_lift_equip: hasLiftEquip === 'true',
insurance_expire_on: moment(insuranceExpireOn).format('MM/DD/YYYY'),
title_registration_on: moment(titleRegistrationOn).format('MM/DD/YYYY'),
emission_test_on: moment(emissionTestOn).format('MM/DD/YYYY'),
oil_change_date: moment(oilChangeDate).format('MM/DD/YYYY')
};
dispatch(updateVehicle({id: params.id, data, redirectFun: redirectTo}));
}
// System fields
status: 'active'
};
};
const validateVehicle = () => {
const errors = [];
// Required fields validation
if (!vehicleNumber || vehicleNumber.trim() === '') {
errors.push('Vehicle Number');
}
if (!tag || tag.trim() === '') {
errors.push('License Plate');
}
if (errors.length > 0) {
window.alert(`Please fill in the following required fields:\n${errors.join('\n')}`);
return false;
}
return true;
};
const saveVehicle = () => {
if (!validateVehicle()) {
return;
}
const data = buildVehicleData();
dispatch(updateVehicle({id: params.id, data, redirectFun: redirectToView}));
}
const triggerShowDeleteModal = () => {
setShowDeleteModal(true);
}
const closeDeleteModal = () => {
setShowDeleteModal(false);
}
const deactivateVehicle = () => {
const data = {
vehicle_number: vehicleNumber,
tag,
ezpass,
gps_tag: gps,
mileage,
capacity,
year,
make,
vehicle_model: vehicleModel,
status: 'inactive',
checklist
};
const data = buildVehicleData();
data.status = 'inactive';
dispatch(deleteVehicle({id: params.id, data}));
setShowDeleteModal(false);
redirectTo();
}
const saveDocuments = () => {
if (selectedMothlyFile && monthlyInspectionDate) {
if (selectedMonthlyFile && monthlyInspectionDate) {
const monthlyFormData = new FormData();
monthlyFormData.append('file', selectedMothlyFile);
VehicleService.uploadVechileFile(monthlyFormData, currentVehicle.id, currentVehicle.vehicle_number, 'monthlyInspection', monthlyInspectionDate);
}
if (selectedYearlyFile && yearlyInspectionDate) {
monthlyFormData.append('file', selectedMonthlyFile);
VehicleService.uploadVechileFile(monthlyFormData, currentVehicle.id, currentVehicle.vehicle_number, 'monthlyInspection', monthlyInspectionDate);
}
if (selectedYearlyFile && yearlyInspectionDate) {
const yearlyFormData = new FormData();
yearlyFormData.append('file', selectedYearlyFile);
VehicleService.uploadVechileFile(yearlyFormData, currentVehicle.id, currentVehicle.vehicle_number, 'yearlyInspection', yearlyInspectionDate);
}
VehicleService.uploadVechileFile(yearlyFormData, currentVehicle.id, currentVehicle.vehicle_number, 'yearlyInspection', yearlyInspectionDate);
}
redirectTo();
}
}
const saveRepair = () => {
const data = {
vehicle: currentVehicle?.id,
repair_date: moment(repairDate).format('MM/DD/YYYY'),
repair_description: repairDescription,
repair_location: repairLocation,
repair_price: repairPrice
}
VehicleRepairService.createNewVehicleRepair(data).then(result => {
const record = result.data;
const formData = new FormData();
formData.append('file', selectedRepairFile);
VehicleService.uploadVechileFile(formData, currentVehicle.id, record.id, 'repair', repairDate).then(() => redirectTo());
})
}
const saveRepair = () => {
const data = {
vehicle: currentVehicle?.id,
part_name: repairPartName,
repair_date: formatDateForBackend(repairReplacementDate),
mileage_at_replacement: repairMileage,
quantity: repairQuantity,
repair_price: repairCost,
repair_location: repairLocation,
next_replacement_reminder: repairNextReminder
};
VehicleRepairService.createNewVehicleRepair(data).then(result => {
const record = result.data;
if (repairReceiptFile) {
const formData = new FormData();
formData.append('file', repairReceiptFile);
VehicleService.uploadVechileFile(formData, currentVehicle.id, record.id, 'repair', repairReplacementDate).then(() => redirectTo());
} else {
redirectTo();
}
});
}
// Custom styles for react-select
const selectStyles = {
control: (baseStyles) => ({
...baseStyles,
minWidth: '200px',
borderRadius: '8px'
}),
indicatorSeparator: () => ({
display: 'none'
}),
dropdownIndicator: (baseStyles) => ({
...baseStyles,
paddingRight: '8px',
color: '#333',
'&:hover': {
color: '#000',
},
}),
valueContainer: (baseStyles) => ({
...baseStyles,
height: '43px',
padding: '0 8px',
}),
input: (baseStyles) => ({
...baseStyles,
margin: '0px',
padding: '0px',
height: '30px',
width: '290px'
}),
singleValue: (baseStyles) => ({
...baseStyles,
margin: '0px',
}),
};
return (
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Transportation</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/trans-routes/dashboard">Transportation</Breadcrumb.Item>
<Breadcrumb.Item href="/vehicles/list">
Vehicles Information
</Breadcrumb.Item>
<Breadcrumb.Item active>
@ -184,223 +326,325 @@ const UpdateVehicle = () => {
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>Update Vehicle Information <button className="btn btn-link btn-sm" onClick={() => {redirectTo()}}>Back</button></h4>
<h4>Update Vehicle Information <button className="btn btn-link btn-sm" onClick={() => {redirectTo()}}>Back</button></h4>
</div>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="basicInfo" id="customers-tab">
<Tab eventKey="basicInfo" title="Basic Information">
<h6 className="text-primary">Vehicle Information</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Vehicle Number
<span className="required">*</span>
</div>
<input type="number" placeholder="e.g.,1" value={vehicleNumber || ''} onChange={e => setVehicleNumber(e.target.value)}/>
</div>
<div className="me-4"><div className="field-label">Seating Capacity</div><input type="number" value={capacity || ''} placeholder="e.g.,12" onChange={e => setCapacity(e.target.value)}/></div>
<div className="me-4"><div className="field-label">Mileage</div><input type="number" value={mileage || ''} placeholder="e.g.,48000" onChange={e => setMileage(e.target.value)}/></div>
<div className="me-4"><div className="field-label">Make</div><input type="text" value={make || ''} placeholder="e.g.,Ford" onChange={e => setMake(e.target.value)}/></div>
<div className="me-4"><div className="field-label">Model</div><input type="text" value={vehicleModel || ''} placeholder="e.g.,T350" onChange={e => setVehicleModel(e.target.value)}/></div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4"><div className="field-label">Year</div><input type="text" value={year || ''} placeholder="e.g.,2016" onChange={e => setYear(e.target.value)}/></div>
<div className="me-4"><div className="field-label">Vin Number</div><input type="text" value={vin || ''} placeholder="e.g, 1FBAX2CM9KA34959" onChange={e => setVin(e.target.value)}/></div>
<div className="me-4"><div className="field-label">Licence Plate</div><input type="text" value={tag || ''} placeholder="e.g.,91579HT" onChange={e => setTag(e.target.value)}/></div>
<div className="me-4"><div className="field-label">GPS ID</div><input type="text" value={gps || ''} placeholder="e.g.,609671" onChange={e => setGps(e.target.value)}/></div>
<div className="me-4"><div className="field-label">EZPass</div><input type="text" value={ezpass || ''} placeholder="e.g.,NY12345" onChange={e => setEzpass(e.target.value)}/></div>
<div className="me-4">
<div className="app-main-content-list-container form-page">
<div className="app-main-content-list-func-container">
<Tabs activeKey={activeTab} onSelect={(k) => setActiveTab(k)} id="vehicles-tab">
<Tab eventKey="basicInfo" title="Basic Information">
<h6 className="text-primary">Basic Information</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Vehicle Number <span className="required">*</span></div>
<input type="text" placeholder="e.g., 101" value={vehicleNumber} onChange={e => setVehicleNumber(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Responsible Driver</div>
<Select
value={responsibleDriver}
onChange={setResponsibleDriver}
options={[{value: '', label: ''}, ...drivers.map(driver => ({
value: driver?.id || '',
label: driver?.name || '',
}))]}
styles={selectStyles}
placeholder="e.g., John Smith"
isClearable
/>
</div>
<div className="me-4">
<div className="field-label">Seating Capacity</div>
<select value={capacity} onChange={e => setCapacity(e.target.value)}>
<option value="">Select...</option>
{SEATING_CAPACITY_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
<div className="me-4">
<div className="field-label">Mileage</div>
<input type="number" placeholder="e.g., 48000" value={mileage} onChange={e => setMileage(e.target.value)}/>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Make</div>
<input type="text" placeholder="e.g., Ford" value={make} onChange={e => setMake(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Model</div>
<input type="text" placeholder="e.g., T350" value={vehicleModel} onChange={e => setVehicleModel(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Year</div>
<input type="text" placeholder="e.g., 2018" value={year} onChange={e => setYear(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">VIN Number</div>
<input type="text" placeholder="e.g., 1FBAX2CM9KKA34959" value={vin} onChange={e => setVin(e.target.value)}/>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">License Plate <span className="required">*</span></div>
<input type="text" placeholder="e.g., 91579HT" value={tag} onChange={e => setTag(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">GPS ID</div>
<input type="text" placeholder="e.g., 609671" value={gps} onChange={e => setGps(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">E-ZPass</div>
<input type="text" placeholder="e.g., NY12345" value={ezpass} onChange={e => setEzpass(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Lift Equipped</div>
<select value={hasLiftEquip} onChange={e => setHasLiftEquip(e.target.value)}>
<option value=""></option>
<option value="true">Yes</option>
<option value="false">No</option>
</select>
<select value={hasLiftEquip} onChange={e => setHasLiftEquip(e.target.value)}>
<option value="">Select...</option>
{Object.keys(LIFT_EQUIPPED).map(key => (
<option key={key} value={LIFT_EQUIPPED[key]}>{LIFT_EQUIPPED_TEXT[LIFT_EQUIPPED[key]]}</option>
))}
</select>
</div>
</div>
<h6 className="text-primary">Vehicle Maintenance & Compliance</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Last Oil Change Date <span className="field-blurb float-right">1-month due cycle </span></div>
<DatePicker selected={oilChangeDate} onChange={(v) => setOilChangeDate(v)} />
</div>
<div className="me-4">
<div className="field-label">Last Emissions Inspection Date <span className="field-blurb float-right">1-year due cycle </span></div>
<DatePicker selected={emissionTestOn} onChange={(v) => setEmissionTestOn(v)} />
</div>
<div className="me-4">
<div className="field-label">Insurance Expiration Date <span className="field-blurb float-right">1-year due cycle </span></div>
<DatePicker selected={insuranceExpireOn} onChange={(v) => setInsuranceExpireOn(v)} />
</div>
<div className="me-4">
<div className="field-label">Title Registration Date <span className="field-blurb float-right">1-year due cycle </span></div>
<DatePicker selected={titleRegistrationOn} onChange={(v) => setTitleRegistrationOn(v)} />
</div>
</div>
<h6 className="text-primary">Check List</h6>
<div className="app-main-content-fields-section column">
{checklist.map((item, index) => (<div className="mb-4" key={index}><input type="text" value={item} onChange={(e) => setChecklist([...checklist].map((a, index1) => {if (index1 === index) {return e.target.value;} return a;}))}/>
<button className="btn btn-link btn-sm" onClick={(e) => setChecklist([...checklist].filter((value, index1) => index1 != index))}>Remove</button>
</div>))}
<button className="btn btn-link" onClick={() => addItemToArray()}>+Add New Item</button>
</div>
<h6 className="text-primary">Additional Information</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Note</div>
<textarea placeholder="Any Extra Details" value={note || ''} onChange={e => setNote(e.target.value)}/>
</div>
</div>
{error && <div className="col-md-12 mb-4 alert alert-danger" role="alert">
{error}
</div>}
<div className="col-md-12 col-sm-12 col-xs-12">
<button className="btn btn-default btn-sm float-right" onClick={() => redirectTo()}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => saveVechile()}> Save </button>
</div>
</Tab>
<Tab eventKey="documents" title="Documents">
<h6 className="text-primary">Yearly Vehicle Inspection</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Vehicle Inspection Date<span className="required">*</span></div>
<DatePicker selected={yearlyInspectionDate} onChange={(v) => setYearlyInspectionDate(v)} />
</div>
<div className="me-4">
<div className="field-label">Yearly Vehicle Inspection Sheet<span className="required">*</span></div>
<label className="custom-file-upload">
<Upload width={20} color={"#fff"} className="me-2"></Upload> Upload Files
<input
type="file"
onChange={(e) => setSelectedYearlyFile(e.target.files[0])}
/>
</label>
<div className="file-name">{ selectedYearlyFile && selectedYearlyFile?.name }</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Fuel Type</div>
<select value={fuelType} onChange={e => setFuelType(e.target.value)}>
<option value="">Select...</option>
{Object.keys(FUEL_TYPE).map(key => (
<option key={key} value={FUEL_TYPE[key]}>{FUEL_TYPE_TEXT[FUEL_TYPE[key]]}</option>
))}
</select>
</div>
</div>
<div className="me-4">
<div className="field-label">Title</div>
<select value={title} onChange={e => setTitle(e.target.value)}>
<option value="">Select...</option>
{Object.keys(VEHICLE_TITLE).map(key => (
<option key={key} value={VEHICLE_TITLE[key]}>{VEHICLE_TITLE_TEXT[VEHICLE_TITLE[key]]}</option>
))}
</select>
</div>
{title === 'other' && (
<div className="me-4">
<div className="field-label">Title (Other)</div>
<input type="text" placeholder="Please specify..." value={titleOther} onChange={e => setTitleOther(e.target.value)}/>
</div>
)}
</div>
<h6 className="text-primary">Monthly Vehicle Inspection</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Vehicle Inspection Date <span className="required">*</span></div>
<DatePicker selected={monthlyInspectionDate} onChange={(v) => setMonthlyInspectionDate(v)} />
</div>
<div className="me-4">
<div className="field-label">Monthly Vehicle Inspection Sheet<span className="required">*</span></div>
<label className="custom-file-upload">
<Upload width={20} color={"#fff"} className="me-2"></Upload> Upload Files
<input
type="file"
onChange={(e) => setSelectedMonthlyFile(e.target.files[0])}
/>
</label>
<div className="file-name">{ selectedMothlyFile && selectedMothlyFile?.name }</div>
<h6 className="text-primary">Check List</h6>
<div className="app-main-content-fields-section column">
{checklist.map((item, index) => (
<div className="mb-4" key={index}>
<input type="text" placeholder="e.g., Tire pressure" value={item} onChange={(e) => setChecklist([...checklist].map((a, index1) => {if (index1 === index) {return e.target.value;} return a;}))}/>
<button className="btn btn-link btn-sm" onClick={(e) => setChecklist([...checklist].filter((value, index1) => index1 != index))}>Remove</button>
</div>
))}
<button className="btn btn-link" onClick={() => addItemToArray()}>+Add New Item</button>
</div>
<h6 className="text-primary">Additional Information</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Notes and Attachments</div>
<textarea placeholder="e.g., Vehicle assigned to Route A" value={note} onChange={e => setNote(e.target.value)} rows={4} style={{width: '400px'}}/>
</div>
</div>
<div className="col-md-12 col-sm-12 col-xs-12">
<button className="btn btn-default btn-sm float-right" onClick={() => redirectTo()}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => saveDocuments()}> Save </button>
</div>
</Tab>
<Tab eventKey="Repair Records" title="Repair Records">
<h6 className="text-primary">Repair Log</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Repair Date <span className="required">*</span></div>
<DatePicker selected={repairDate} onChange={(v) => setRepairDate(v)} />
</div>
<div className="me-4"><div className="field-label">Cost <span className="required">*</span></div>
<input type="text" value={repairPrice || ''} placeholder="e.g.,$75" onChange={e => setRepairPrice(e.target.value)}/>
</div>
<div className="me-4"><div className="field-label">Repair Location <span className="required">*</span></div>
<input type="text" value={repairLocation || ''} placeholder="e.g.,LocalGarage" onChange={e => setRepairLocation(e.target.value)}/>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Description <span className="required">*</span></div>
<textarea value={repairDescription || ''} onChange={e => setRepairDescription(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Upload Maintenance Files</div>
</div>
{error && <div className="col-md-12 mb-4 alert alert-danger" role="alert">
{error}
</div>}
<div className="list row mb-5">
<div className="col-md-12 col-sm-12 col-xs-12">
<button className="btn btn-default btn-sm float-right" onClick={() => redirectTo()}> Cancel </button>
<button className="btn btn-danger btn-sm me-2 mb-2" onClick={() => triggerShowDeleteModal()}> Delete </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => saveVehicle()}> Save </button>
</div>
</div>
</Tab>
<Tab eventKey="complianceDeadlines" title="Compliance & Deadlines">
<h6 className="text-primary">Compliance & Deadlines</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Insurance Expiration Date</div>
<DatePicker
selected={insuranceStartDate}
onChange={(date) => setInsuranceStartDate(date)}
dateFormat="MM/dd/yyyy"
placeholderText="e.g., 03/01/2024"
className="form-control"
/>
</div>
<div className="me-4">
<div className="field-label">Vehicle Registration Date</div>
<DatePicker
selected={vehicleRegistrationDate}
onChange={(date) => setVehicleRegistrationDate(date)}
dateFormat="MM/dd/yyyy"
placeholderText="e.g., 03/01/2024"
className="form-control"
/>
</div>
</div>
{error && <div className="col-md-12 mb-4 alert alert-danger" role="alert">
{error}
</div>}
<div className="list row mb-5">
<div className="col-md-12 col-sm-12 col-xs-12">
<button className="btn btn-default btn-sm float-right" onClick={() => redirectTo()}> Cancel </button>
<button className="btn btn-danger btn-sm me-2 mb-2" onClick={() => triggerShowDeleteModal()}> Delete </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => saveVehicle()}> Save </button>
</div>
</div>
</Tab>
<Tab eventKey="documents" title="Documents & Records">
<h6 className="text-primary">Yearly Inspection</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Yearly Inspection Date</div>
<DatePicker
selected={yearlyInspectionDate}
onChange={(date) => setYearlyInspectionDate(date)}
dateFormat="MM/dd/yyyy"
placeholderText="e.g., 03/01/2024"
className="form-control"
/>
</div>
<div className="me-4">
<div className="field-label">Yearly Inspection File</div>
<label className="custom-file-upload">
<Upload width={20} color={"#fff"} className="me-2"></Upload> Upload Files
<input
type="file"
onChange={(e) => setSelectedRepairFile(e.target.files[0])}
/>
<Upload width={20} color={"#fff"} className="me-2"></Upload> Upload
<input type="file" onChange={(e) => setSelectedYearlyFile(e.target.files[0])}/>
</label>
<div className="file-name">{ selectedRepairFile && selectedRepairFile?.name }</div>
</div>
</div>
<div className="col-md-12 col-sm-12 col-xs-12">
<button className="btn btn-default btn-sm float-right" onClick={() => redirectTo()}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => saveRepair()}> Save </button>
</div>
</Tab>
</Tabs>
<div className="file-name">{selectedYearlyFile?.name}</div>
</div>
</div>
<h6 className="text-primary">Monthly Inspection</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Monthly Inspection Date</div>
<DatePicker
selected={monthlyInspectionDate}
onChange={(date) => setMonthlyInspectionDate(date)}
dateFormat="MM/dd/yyyy"
placeholderText="e.g., 03/01/2024"
className="form-control"
/>
</div>
<div className="me-4">
<div className="field-label">Monthly Inspection File</div>
<label className="custom-file-upload">
<Upload width={20} color={"#fff"} className="me-2"></Upload> Upload
<input type="file" onChange={(e) => setSelectedMonthlyFile(e.target.files[0])}/>
</label>
<div className="file-name">{selectedMonthlyFile?.name}</div>
</div>
</div>
<div className="list row mb-3">
<div className="col-md-12 col-sm-12 col-xs-12">
<button className="btn btn-primary btn-sm" onClick={() => saveDocuments()}> Upload Documents </button>
</div>
</div>
<h6 className="text-primary">Repair & Maintenance Record</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Part Name</div>
<select value={repairPartName} onChange={e => setRepairPartName(e.target.value)}>
<option value="">Select...</option>
{Object.keys(REPAIR_PART_NAME).map(key => (
<option key={key} value={REPAIR_PART_NAME[key]}>{REPAIR_PART_NAME_TEXT[REPAIR_PART_NAME[key]]}</option>
))}
</select>
</div>
<div className="me-4">
<div className="field-label">Replacement Date</div>
<DatePicker
selected={repairReplacementDate}
onChange={(date) => setRepairReplacementDate(date)}
dateFormat="MM/dd/yyyy"
placeholderText="e.g., 03/01/2024"
className="form-control"
/>
</div>
<div className="me-4">
<div className="field-label">Mileage at Replacement</div>
<input type="text" placeholder="e.g., 48,000" value={repairMileage} onChange={e => setRepairMileage(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Quantity</div>
<select value={repairQuantity} onChange={e => setRepairQuantity(e.target.value)}>
<option value="">Select...</option>
{QUANTITY_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Cost</div>
<input type="text" placeholder="e.g., $250.00" value={repairCost} onChange={e => setRepairCost(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Location</div>
<input type="text" placeholder="e.g., Rockville Auto Center" value={repairLocation} onChange={e => setRepairLocation(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Receipt Upload</div>
<label className="custom-file-upload">
<Upload width={20} color={"#fff"} className="me-2"></Upload> Upload
<input type="file" onChange={(e) => setRepairReceiptFile(e.target.files[0])}/>
</label>
<div className="file-name">{repairReceiptFile?.name}</div>
</div>
<div className="me-4">
<div className="field-label">Next Replacement Reminder</div>
<input type="text" placeholder="e.g., 78,000" value={repairNextReminder} onChange={e => setRepairNextReminder(e.target.value)}/>
</div>
</div>
{error && <div className="col-md-12 mb-4 alert alert-danger" role="alert">
{error}
</div>}
<div className="list row mb-5">
<div className="col-md-12 col-sm-12 col-xs-12">
<button className="btn btn-default btn-sm float-right" onClick={() => redirectTo()}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => saveRepair()}> Save Repair Record </button>
</div>
</div>
</Tab>
</Tabs>
<div className="list-func-panel">
<button className="btn btn-primary" onClick={() => deactivateVehicle()}><Archive size={16} className="me-2"></Archive>Archive</button>
</div>
</div>
</div>
{/* <div className="list row mb-4">
<div className="col-md-12 text-primary">
<h5>Update Vehicle <button className="btn btn-link btn-sm" onClick={() => {redirectTo()}}>Back</button></h5>
</div>
</div>
<div className="list row mb-4">
<div className="col-md-4 mb-4">
<div>Vehicle Number(*):</div> <input type="number" value={vehicleNumber || ''} onChange={e => setVehicleNumber(e.target.value)}/>
</div>
<div className="col-md-4 mb-4">
<div>Tag(*):</div> <input type="text" value={tag || ''} onChange={e => setTag(e.target.value)}/>
</div>
<div className="col-md-4 mb-4">
<div>EZ Pass:</div> <input type="text" value={ezpass || ''} onChange={e => setEzpass(e.target.value)}/>
</div>
<div className="col-md-4 mb-4">
<div>GPS:</div> <input type="text" value={gps || ''} onChange={e => setGps(e.target.value)}/>
</div>
<div className="col-md-4 mb-4">
<div>Make:</div> <input type="text" value={make || ''} onChange={e => setMake(e.target.value)}/>
</div>
<div className="col-md-4 mb-4">
<div>Model:</div> <input type="text" value={vehicleModel || ''} onChange={e => setVehicleModel(e.target.value)}/>
</div>
<div className="col-md-4 mb-4">
<div>Year:</div> <input type="text" value={year || ''} onChange={e => setYear(e.target.value)}/>
</div>
<div className="col-md-4 mb-4">
<div>Mileage(*):</div> <input type="number" value={mileage || ''} onChange={e => setMileage(e.target.value)}/>
</div>
<div className="col-md-4 mb-4">
<div>Capacity(*):</div> <input type="number" value={capacity || ''} onChange={e => setCapacity(e.target.value)}/>
</div>
<div className="col-md-12 mb-4">
<div>Checklist(*):</div>
{checklist.map((item, index) => (<div className="mb-4" key={index}><input type="text" value={item} onChange={(e) => setChecklist([...checklist].map((a, index1) => {if (index1 === index) {return e.target.value;} return a;}))}/>
<button className="btn btn-link btn-sm" onClick={(e) => setChecklist([...checklist].filter((value, index1) => index1 != index))}>Remove</button>
</div>))}
<button className="btn btn-link" onClick={() => addItemToArray()}>+Add New Item</button>
</div>
</div>
<div className="list row mb-5">
{error && <div className="col-md-12 mb-4 alert alert-danger" role="alert">
{error}
</div>}
<div className="col-md-6 col-sm-6 col-xs-12">
<button className="btn btn-primary btn-sm me-2 mb-2" onClick={() => saveVechile()}> Save </button>
<button className="btn btn-danger btn-sm me-2 mb-2" onClick={() => deactivateVehicle()}> Delete </button>
<button className="btn btn-default btn-sm mb-2" onClick={() => redirectTo()}> Cancel </button>
</div>
</div> */}
<Modal show={showDeleteModal} onHide={() => closeDeleteModal()}>
<Modal.Header closeButton>
<Modal.Title>Delete Vehicle</Modal.Title>
</Modal.Header>
<Modal.Body>
<div>Are you sure you want to delete this vehicle?</div>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={() => closeDeleteModal()}>
No
</Button>
<Button variant="primary" onClick={() => deactivateVehicle()}>
Yes
</Button>
</Modal.Footer>
</Modal>
</>
);
};
export default UpdateVehicle;
export default UpdateVehicle;

View File

@ -68,24 +68,24 @@ const VehicleList = () => {
useEffect(() => {
if (showInactive) {
setFilteredVehicles(vehicles && vehicles.filter(item =>
(item?.vehicle_number?.toString()?.includes(keyword.toLowerCase()) ||
item?.tag?.toLowerCase()?.includes(keyword.toLowerCase()) ||
item?.ezpass?.toLowerCase()?.includes(keyword.toLowerCase()) ||
item?.gps_tag?.toLowerCase()?.includes(keyword.toLowerCase()) ||
item?.make?.toLowerCase()?.includes(keyword.toLowerCase()) ||
item?.vehicle_model?.toLowerCase()?.includes(keyword.toLowerCase()) ||
item?.year?.toLowerCase()?.includes(keyword.toLowerCase())) &&
(item?.vehicle_number?.toString()?.startsWith(keyword.toLowerCase()) ||
item?.tag?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.ezpass?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.gps_tag?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.make?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.vehicle_model?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.year?.toLowerCase()?.startsWith(keyword.toLowerCase())) &&
item?.status?.toLowerCase() !== 'active'
))
} else {
setFilteredVehicles(vehicles && vehicles.filter(item =>
(item?.vehicle_number?.toString()?.includes(keyword.toLowerCase()) ||
item?.tag?.toLowerCase()?.includes(keyword.toLowerCase()) ||
item?.ezpass?.toLowerCase()?.includes(keyword.toLowerCase()) ||
item?.gps_tag?.toLowerCase()?.includes(keyword.toLowerCase()) ||
item?.make?.toLowerCase()?.includes(keyword.toLowerCase()) ||
item?.vehicle_model?.toLowerCase()?.includes(keyword.toLowerCase()) ||
item?.year?.toLowerCase()?.includes(keyword.toLowerCase())) &&
(item?.vehicle_number?.toString()?.startsWith(keyword.toLowerCase()) ||
item?.tag?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.ezpass?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.gps_tag?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.make?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.vehicle_model?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.year?.toLowerCase()?.startsWith(keyword.toLowerCase())) &&
item?.status?.toLowerCase() === 'active'
))
}
@ -203,7 +203,7 @@ const VehicleList = () => {
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Transportation</Breadcrumb.Item>
<Breadcrumb.Item href="/trans-routes/dashboard">Transportation</Breadcrumb.Item>
<Breadcrumb.Item active>
Vehicle Information
</Breadcrumb.Item>
@ -214,7 +214,7 @@ const VehicleList = () => {
</h4>
</div>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-container list-page">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="activeVehicles" id="vehicles-tab" onSelect={(k) => showArchive(k)}>
<Tab eventKey="activeVehicles" title="Active Vehicles">

View File

@ -1,18 +1,23 @@
import React, {useState, useEffect} from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useSelector,useDispatch } from "react-redux";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { AuthService, VehicleRepairService, VehicleService } from "../../services";
import { vehicleSlice, selectVehicleError } from "./../../store";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab } from "react-bootstrap";
import { Download, PencilSquare, Archive } from "react-bootstrap-icons";
import moment from "moment";
import { Export } from "../../shared/components";
import {
FUEL_TYPE_TEXT,
VEHICLE_TITLE_TEXT,
REPAIR_PART_NAME_TEXT
} from "../../shared";
const ViewVehicle = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const urlParams = useParams();
const [currentVehicle, setCurrentVehicle] = useState(undefined);
const [searchParams] = useSearchParams();
const [monthlyDocs, setMonthlyDocs] = useState([]);
const [yearlyDocs, setYearlyDocs] = useState([]);
const [repairs, setRepairs] = useState([]);
@ -23,36 +28,37 @@ const ViewVehicle = () => {
const [selectedItemsMonthly, setSelectedItemsMonthly] = useState([]);
const [selectedItemsYearly, setSelectedItemsYearly] = useState([]);
const [selectedItemsRepair, setSelectedItemsRepair] = useState([]);
const [filteredMonthlyDocs, setFilteredMonthlyDocs] = useState(monthlyDocs);
const [filteredMonthlyDocs, setFilteredMonthlyDocs] = useState(monthlyDocs);
const [filteredYearlyDocs, setFilteredYearlyDocs] = useState(yearlyDocs);
const [filteredRepairs, setFilteredRepairs] = useState(repairs);
const { updateVehicle, deleteVehicle, fetchAllVehicles } = vehicleSlice.actions;
const [currentTab, setCurrentTab] = useState('basicInfo')
const [currentTab, setCurrentTab] = useState(searchParams.get('tab') || 'basicInfo');
const [currentVehicle, setCurrentVehicle] = useState(undefined);
const redirectTo = () => {
navigate(`/vehicles/list`)
navigate(`/vehicles/list`);
}
const goToEdit = (id) => {
navigate(`/vehicles/edit/${id}?redirect=list`)
navigate(`/vehicles/edit/${id}?redirect=list&tab=${currentTab}`);
}
const download = () => {
const downloadArr = [...selectedItemsMonthly, ...selectedItemsYearly];
downloadArr.forEach((url) => {
const a = document.createElement('a');
a.href= url;
a.href = url;
a.download = '';
document.body.append(a);
a.click();
document.body.removeChild(a);
})
});
}
const deactivateVehicle = () => {
const data = {
status: 'inactive'
};
status: 'inactive'
};
dispatch(deleteVehicle({id: urlParams.id, data}));
redirectTo();
}
@ -64,7 +70,6 @@ const ViewVehicle = () => {
if (prefix) {
const arr2 = prefix.split('_');
const dateNumber = arr2[arr2.length - 1];
return dateNumber ? new Date(parseInt(dateNumber)).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'}) : moment().format('MM/DD/YYYY');
} else {
return moment().format('MM/DD/YYYY');
@ -78,7 +83,6 @@ const ViewVehicle = () => {
};
const getAllRepairs = async (vid) => {
const v_repairs = (await VehicleRepairService.getAll(vid)).data;
console.log('repairs', v_repairs);
setRepairs(v_repairs);
}
if (!AuthService.canViewVechiles()) {
@ -91,7 +95,7 @@ const ViewVehicle = () => {
setCurrentVehicle(data.data);
getAllDocuments(data.data?.id, data.data?.vehicle_number);
getAllRepairs(data.data?.id);
})
});
} else {
getAllDocuments(currentVehicle?.id, currentVehicle?.vehicle_number);
getAllRepairs(currentVehicle?.id);
@ -104,8 +108,14 @@ const ViewVehicle = () => {
}, [keyword, yearlyDocs, monthlyDocs]);
useEffect(() => {
setFilteredRepairs(repairs?.filter(item => item?.repair_description?.toLowerCase().includes(keyword.toLowerCase()) || item?.repair_date?.includes(keyword.toLowerCase()) || item?.repair_location?.toLowerCase().includes(keyword.toLowerCase()) || item?.repair_price?.toLowerCase().includes(keyword.toLowerCase()) ));
}, [keyword, repairs])
setFilteredRepairs(repairs?.filter(item =>
item?.repair_description?.toLowerCase().includes(keyword.toLowerCase()) ||
item?.repair_date?.includes(keyword.toLowerCase()) ||
item?.repair_location?.toLowerCase().includes(keyword.toLowerCase()) ||
item?.repair_price?.toLowerCase().includes(keyword.toLowerCase()) ||
item?.part_name?.toLowerCase().includes(keyword.toLowerCase())
));
}, [keyword, repairs]);
useEffect(() => {
const newYearlyDocs = [...yearlyDocs];
@ -114,7 +124,7 @@ const ViewVehicle = () => {
});
setYearlyDocs(
sortingMonthly.order === 'asc' ? sortedYearlyDocs : sortedYearlyDocs.reverse()
)
);
}, [sortingYearly]);
useEffect(() => {
@ -124,7 +134,7 @@ const ViewVehicle = () => {
});
setMonthlyDocs(
sortingMonthly.order === 'asc' ? sortedMonthlyDocs : sortedMonthlyDocs.reverse()
)
);
}, [sortingMonthly]);
useEffect(() => {
@ -134,73 +144,41 @@ const ViewVehicle = () => {
});
setRepairs(
sortingRepair.order === 'asc' ? sortedRepairs : sortedRepairs.reverse()
)
);
}, [sortingRepair]);
// Helper to format Yes/No values
const formatYesNo = (value) => {
if (value === true) return 'Yes';
if (value === false) return 'No';
return value || '';
};
const columnsMonthly = [
{
key: 'name',
label: 'Monthly Vehicle Inspection Sheet'
},
{
key: 'inspectionDate',
label: 'Vehicle Inspection Date'
},
{
key: 'createdAt',
label: 'Date Added'
}
{ key: 'name', label: 'Monthly Vehicle Inspection Sheet' },
{ key: 'inspectionDate', label: 'Vehicle Inspection Date' },
{ key: 'createdAt', label: 'Date Added' }
];
const columnsYearly = [
{
key: 'name',
label: 'Yearly Vehicle Inspection Sheet'
},
{
key: 'inspectionDate',
label: 'Vehicle Inspection Date'
},
{
key: 'createdAt',
label: 'Date Added'
}
{ key: 'name', label: 'Yearly Vehicle Inspection Sheet' },
{ key: 'inspectionDate', label: 'Vehicle Inspection Date' },
{ key: 'createdAt', label: 'Date Added' }
];
const columnsRepair = [
{
key: 'repair_description',
label: 'Repair Description',
show: true
},
{
key: 'repair_date',
label: 'Repair Date',
show: true
},
{
key: 'repair_price',
label: 'Cost',
show: true
},
{
key: 'repair_location',
label: 'Repair Location',
show: true
},
{
key: 'create_date',
label: 'Date Added',
show: true
}
{ key: 'part_name', label: 'Part Name', show: true },
{ key: 'repair_date', label: 'Replacement Date', show: true },
{ key: 'mileage_at_replacement', label: 'Mileage', show: true },
{ key: 'quantity', label: 'Quantity', show: true },
{ key: 'repair_price', label: 'Cost', show: true },
{ key: 'repair_location', label: 'Location', show: true },
{ key: 'next_replacement_reminder', label: 'Next Reminder', show: true },
{ key: 'create_date', label: 'Date Added', show: true }
];
const sortTableWithFieldMonthly = (key) => {
let newSorting = {
key,
order: 'asc',
}
let newSorting = { key, order: 'asc' };
if (sortingMonthly.key === key && sortingMonthly.order === 'asc') {
newSorting = {...newSorting, order: 'desc'};
}
@ -208,11 +186,7 @@ const ViewVehicle = () => {
}
const sortTableWithFieldYearly = (key) => {
let newSorting = {
key,
order: 'asc',
}
let newSorting = { key, order: 'asc' };
if (sortingYearly.key === key && sortingYearly.order === 'asc') {
newSorting = {...newSorting, order: 'desc'};
}
@ -220,11 +194,7 @@ const ViewVehicle = () => {
}
const sortTableWithFieldRepair = (key) => {
let newSorting = {
key,
order: 'asc',
}
let newSorting = { key, order: 'asc' };
if (sortingRepair.key === key && sortingRepair.order === 'asc') {
newSorting = {...newSorting, order: 'desc'};
}
@ -252,13 +222,13 @@ const ViewVehicle = () => {
const toggleSelectedAllItemsRepair = () => {
if (selectedItemsRepair.length !== filteredRepairs.length || filteredRepairs.length === 0) {
const newSelectedItems = [...filteredRepairs].map((doc) => doc.id);
selectedItemsRepair(newSelectedItems);
setSelectedItemsRepair(newSelectedItems);
} else {
selectedItemsRepair([]);
setSelectedItemsRepair([]);
}
}
const toggleItemYearly = (id) => {
const toggleItemYearly = (id) => {
if (selectedItemsYearly.includes(id)) {
const newSelectedItems = [...selectedItemsYearly].filter((item) => item !== id);
setSelectedItemsYearly(newSelectedItems);
@ -268,7 +238,7 @@ const ViewVehicle = () => {
}
}
const toggleItemMonthly = (id) => {
const toggleItemMonthly = (id) => {
if (selectedItemsMonthly.includes(id)) {
const newSelectedItems = [...selectedItemsMonthly].filter((item) => item !== id);
setSelectedItemsMonthly(newSelectedItems);
@ -278,7 +248,7 @@ const ViewVehicle = () => {
}
}
const toggleItemRepair = (id) => {
const toggleItemRepair = (id) => {
if (selectedItemsRepair.includes(id)) {
const newSelectedItems = [...selectedItemsRepair].filter((item) => item !== id);
setSelectedItemsRepair(newSelectedItems);
@ -309,7 +279,7 @@ const ViewVehicle = () => {
}
const checkSelectAllRepair = () => {
return selectedItemsRepair.length === selectedItemsRepair.length && selectedItemsRepair.length > 0;
return selectedItemsRepair.length === filteredRepairs.length && selectedItemsRepair.length > 0;
}
const changeTab = (k) => {
@ -320,107 +290,116 @@ const ViewVehicle = () => {
setSortingRepair({key: '', order: ''});
}
const tableMonthly = <div className="list row mb-4">
<div className="col-md-12">
<table className="personnel-info-table">
<thead>
<tr>
<th className="th-checkbox"><input type="checkbox" checked={checkSelectAllMonthly()} onClick={() => toggleSelectedAllItemsMonthly()}></input></th>
<th className="th-index">No.</th>
{
columnsMonthly.map((column, index) => <th className="sortable-header" key={index}>
{column.label} <span className="float-right" onClick={() => sortTableWithFieldMonthly(column.key)}><img src={`/images/${getSortingImgMonthly(column.key)}.png`}></img></span>
</th>)
}
</tr>
</thead>
<tbody>
{
filteredMonthlyDocs.map((doc, index) => <tr key={doc.url}>
<td className="td-checkbox"><input type="checkbox" checked={selectedItemsMonthly.includes(doc?.url)} onClick={()=>toggleItemMonthly(doc?.url)}/></td>
<td className="td-index">{index + 1}</td>
<td> <PencilSquare size={16} className="clickable me-2" onClick={() => goToEdit(currentVehicle?.id)}></PencilSquare>
<a className="btn btn-link btn-sm" href={doc?.url} target="_blank">{doc?.name}</a> </td>
<td>{doc?.inspectionDate}</td>
<td>{new Date(doc?.createdAt).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'})}</td>
</tr>)
}
</tbody>
</table>
</div>
</div>;
const tableMonthly = (
<div className="list row mb-4">
<div className="col-md-12">
<table className="personnel-info-table">
<thead>
<tr>
<th className="th-checkbox"><input type="checkbox" checked={checkSelectAllMonthly()} onClick={() => toggleSelectedAllItemsMonthly()}></input></th>
<th className="th-index">No.</th>
{columnsMonthly.map((column, index) => (
<th className="sortable-header" key={index}>
{column.label} <span className="float-right" onClick={() => sortTableWithFieldMonthly(column.key)}><img src={`/images/${getSortingImgMonthly(column.key)}.png`}></img></span>
</th>
))}
</tr>
</thead>
<tbody>
{filteredMonthlyDocs.map((doc, index) => (
<tr key={doc.url}>
<td className="td-checkbox"><input type="checkbox" checked={selectedItemsMonthly.includes(doc?.url)} onClick={() => toggleItemMonthly(doc?.url)}/></td>
<td className="td-index">{index + 1}</td>
<td>
<PencilSquare size={16} className="clickable me-2" onClick={() => goToEdit(currentVehicle?.id)}></PencilSquare>
<a className="btn btn-link btn-sm" href={doc?.url} target="_blank">{doc?.name}</a>
</td>
<td>{doc?.inspectionDate}</td>
<td>{new Date(doc?.createdAt).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'})}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
const tableYearly = <div className="list row mb-4">
<div className="col-md-12">
<table className="personnel-info-table">
<thead>
<tr>
<th className="th-checkbox"><input type="checkbox" checked={checkSelectAllYearly()} onClick={() => toggleSelectedAllItemsYearly()}></input></th>
<th className="th-index">No.</th>
{
columnsYearly.map((column, index) => <th className="sortable-header" key={index}>
{column.label} <span className="float-right" onClick={() => sortTableWithFieldYearly(column.key)}><img src={`/images/${getSortingImgYearly(column.key)}.png`}></img></span>
</th>)
}
</tr>
</thead>
<tbody>
{
filteredYearlyDocs.map((doc, index) => <tr key={doc.url}>
<td className="td-checkbox"><input type="checkbox" checked={selectedItemsYearly.includes(doc?.url)} onClick={()=>toggleItemYearly(doc?.url)}/></td>
<td className="td-index">{index + 1}</td>
<td> <PencilSquare size={16} className="clickable me-2" onClick={() => goToEdit(currentVehicle?.id)}></PencilSquare>
<a className="btn btn-link btn-sm" href={doc?.url} target="_blank">{doc?.name}</a> </td>
<td>{doc?.inspectionDate}</td>
<td>{new Date(doc?.createdAt).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'})}</td>
</tr>)
}
</tbody>
</table>
</div>
</div>;
const tableYearly = (
<div className="list row mb-4">
<div className="col-md-12">
<table className="personnel-info-table">
<thead>
<tr>
<th className="th-checkbox"><input type="checkbox" checked={checkSelectAllYearly()} onClick={() => toggleSelectedAllItemsYearly()}></input></th>
<th className="th-index">No.</th>
{columnsYearly.map((column, index) => (
<th className="sortable-header" key={index}>
{column.label} <span className="float-right" onClick={() => sortTableWithFieldYearly(column.key)}><img src={`/images/${getSortingImgYearly(column.key)}.png`}></img></span>
</th>
))}
</tr>
</thead>
<tbody>
{filteredYearlyDocs.map((doc, index) => (
<tr key={doc.url}>
<td className="td-checkbox"><input type="checkbox" checked={selectedItemsYearly.includes(doc?.url)} onClick={() => toggleItemYearly(doc?.url)}/></td>
<td className="td-index">{index + 1}</td>
<td>
<PencilSquare size={16} className="clickable me-2" onClick={() => goToEdit(currentVehicle?.id)}></PencilSquare>
<a className="btn btn-link btn-sm" href={doc?.url} target="_blank">{doc?.name}</a>
</td>
<td>{doc?.inspectionDate}</td>
<td>{new Date(doc?.createdAt).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'})}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
const tableRepair = (
<div className="list row mb-4">
<div className="col-md-12">
<table className="personnel-info-table">
<thead>
<tr>
<th className="th-checkbox"><input type="checkbox" checked={checkSelectAllRepair()} onClick={() => toggleSelectedAllItemsRepair()}></input></th>
<th className="th-index">No.</th>
{columnsRepair.map((column, index) => (
<th className="sortable-header" key={index}>
{column.label} <span className="float-right" onClick={() => sortTableWithFieldRepair(column.key)}><img src={`/images/${getSortingImgRepair(column.key)}.png`}></img></span>
</th>
))}
</tr>
</thead>
<tbody>
{filteredRepairs?.map((repair, index) => (
<tr key={repair.id}>
<td className="td-checkbox"><input type="checkbox" checked={selectedItemsRepair.includes(repair?.id)} onClick={() => toggleItemRepair(repair?.id)}/></td>
<td className="td-index">{index + 1}</td>
<td>{REPAIR_PART_NAME_TEXT[repair?.part_name] || repair?.part_name || repair?.repair_description}</td>
<td>{repair?.repair_date}</td>
<td>{repair?.mileage_at_replacement}</td>
<td>{repair?.quantity}</td>
<td>{repair?.repair_price}</td>
<td>{repair?.repair_location}</td>
<td>{repair?.next_replacement_reminder}</td>
<td>{repair?.create_date ? new Date(repair?.create_date).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'}) : repair?.repair_date}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
const tableRepair = <div className="list row mb-4">
<div className="col-md-12">
<table className="personnel-info-table">
<thead>
<tr>
<th className="th-checkbox"><input type="checkbox" checked={checkSelectAllRepair()} onClick={() => toggleSelectedAllItemsRepair()}></input></th>
<th className="th-index">No.</th>
{
columnsRepair.map((column, index) => <th className="sortable-header" key={index}>
{column.label} <span className="float-right" onClick={() => sortTableWithFieldRepair(column.key)}><img src={`/images/${getSortingImgRepair(column.key)}.png`}></img></span>
</th>)
}
</tr>
</thead>
<tbody>
{
filteredRepairs?.map((repair, index) => <tr key={repair.id}>
<td className="td-checkbox"><input type="checkbox" checked={selectedItemsRepair.includes(repair?.id)} onClick={()=>toggleItemRepair(repair?.id)}/></td>
<td className="td-index">{index + 1}</td>
<td> {repair?.repair_description}</td>
<td>{repair?.repair_date}</td>
<td>{repair?.repair_price}</td>
<td>{repair?.repair_location}</td>
<td>{repair?.create_date ? new Date(repair?.create_date).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'}) : repair?.repair_date}</td>
</tr>)
}
</tbody>
</table>
</div>
</div>;
return (
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item>Transportation</Breadcrumb.Item>
<Breadcrumb.Item active>
<Breadcrumb.Item href="/trans-routes/dashboard">Transportation</Breadcrumb.Item>
<Breadcrumb.Item href="/vehicles/list">
Vehicles Information
</Breadcrumb.Item>
<Breadcrumb.Item active>
@ -429,18 +408,22 @@ const tableRepair = <div className="list row mb-4">
</Breadcrumb>
</div>
<div className="col-md-12 text-primary">
<h4>View Vechile Information <button className="btn btn-link btn-sm" onClick={() => {redirectTo()}}>Back</button></h4>
<h4>View Vehicle Information <button className="btn btn-link btn-sm" onClick={() => {redirectTo()}}>Back</button></h4>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="basicInfo" id="customers-tab" onSelect={k => changeTab(k)}>
<Tab eventKey="basicInfo" title="Basic Information">
<h6 className="text-primary">Vehicle Information</h6>
<div className="app-main-content-fields-section">
<div className="app-main-content-list-container form-page">
<div className="app-main-content-list-func-container">
<Tabs activeKey={currentTab} id="vehicles-tab" onSelect={k => changeTab(k)}>
<Tab eventKey="basicInfo" title="Basic Information">
<h6 className="text-primary">Basic Information</h6>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Vehicle Number</div>
<div className="field-value">{currentVehicle?.vehicle_number}</div>
</div>
<div className="field-body">
<div className="field-label">Responsible Driver</div>
<div className="field-value">{currentVehicle?.responsible_driver}</div>
</div>
<div className="field-body">
<div className="field-label">Seating Capacity</div>
<div className="field-value">{currentVehicle?.capacity}</div>
@ -449,6 +432,8 @@ const tableRepair = <div className="list row mb-4">
<div className="field-label">Mileage</div>
<div className="field-value">{currentVehicle?.mileage}</div>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Make</div>
<div className="field-value">{currentVehicle?.make}</div>
@ -457,87 +442,103 @@ const tableRepair = <div className="list row mb-4">
<div className="field-label">Model</div>
<div className="field-value">{currentVehicle?.vehicle_model}</div>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Year</div>
<div className="field-value">{currentVehicle?.year}</div>
</div>
<div className="field-body">
<div className="field-label">Vin Number</div>
<div className="field-label">VIN Number</div>
<div className="field-value">{currentVehicle?.vin}</div>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Licence Plate</div>
<div className="field-label">License Plate</div>
<div className="field-value">{currentVehicle?.tag}</div>
</div>
<div className="field-body">
<div className="field-label">GPS ID</div>
<div className="field-value">{currentVehicle?.gps}</div>
<div className="field-value">{currentVehicle?.gps_tag}</div>
</div>
<div className="field-body">
<div className="field-label">EZPass</div>
<div className="field-label">E-ZPass</div>
<div className="field-value">{currentVehicle?.ezpass}</div>
</div>
<div className="field-body">
<div className="field-label">Lift Equipped</div>
<div className="field-value">{currentVehicle?.has_lift_equip === true ? 'Yes' : (currentVehicle?.has_lift_equip === false? 'No' : '')}</div>
<div className="field-value">{formatYesNo(currentVehicle?.has_lift_equip)}</div>
</div>
</div>
<h6 className="text-primary">Vehicle Maintenance & Compliance</h6>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Last Oil Change Date <span className="field-blurb float-right">1-month due cycle </span></div>
<div className="field-value">{currentVehicle?.oil_change_date}</div>
<div className="field-label">Fuel Type</div>
<div className="field-value">{FUEL_TYPE_TEXT[currentVehicle?.fuel_type] || currentVehicle?.fuel_type}</div>
</div>
<div className="field-body">
<div className="field-label">Last Emissions Inspection Date <span className="field-blurb float-right">1-year due cycle </span></div>
<div className="field-value">{currentVehicle?.emission_test_on}</div>
</div>
<div className="field-body">
<div className="field-label">Insurance Expiration Date <span className="field-blurb float-right">1-year due cycle </span></div>
<div className="field-value">{currentVehicle?.insurance_expire_on}</div>
</div>
<div className="field-body">
<div className="field-label">Title Registration Date <span className="field-blurb float-right">1-year due cycle </span></div>
<div className="field-value">{currentVehicle?.title_registration_on}</div>
<div className="field-label">Title</div>
<div className="field-value">
{VEHICLE_TITLE_TEXT[currentVehicle?.title] || currentVehicle?.title}
{currentVehicle?.title === 'other' && currentVehicle?.title_other && ` (${currentVehicle?.title_other})`}
</div>
</div>
</div>
<h6 className="text-primary">Check List</h6>
<div className="app-main-content-fields-section column">
<div className="app-main-content-fields-section column">
<ul>
{currentVehicle?.checklist?.map((item) => <li>{item}</li>)}
{currentVehicle?.checklist?.map((item, index) => <li key={index}>{item}</li>)}
</ul>
</div>
<h6 className="text-primary">Additional Information</h6>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Note</div>
<div className="field-label">Notes and Attachments</div>
<div className="field-value">{currentVehicle?.note}</div>
</div>
</div>
</Tab>
<Tab eventKey="documents" title="Documents">
<h6 className="text-primary">Yearly Vehicle Inspection</h6>
<Tab eventKey="complianceDeadlines" title="Compliance & Deadlines">
<h6 className="text-primary">Compliance & Deadlines</h6>
<div className="app-main-content-fields-section">
<div className="field-body">
<div className="field-label">Insurance Expiration Date</div>
<div className="field-value">{currentVehicle?.insurance_start_date || currentVehicle?.insurance_expire_on}</div>
</div>
<div className="field-body">
<div className="field-label">Vehicle Registration Date</div>
<div className="field-value">{currentVehicle?.vehicle_registration_date || currentVehicle?.title_registration_on}</div>
</div>
</div>
</Tab>
<Tab eventKey="documents" title="Documents & Records">
<h6 className="text-primary">Yearly Inspection</h6>
{tableYearly}
<h6 className="text-primary">Monthly Vehicle Inspection</h6>
<h6 className="text-primary">Monthly Inspection</h6>
{tableMonthly}
</Tab>
<Tab eventKey="repairRecords" title="Repair Records">
{tableRepair}
</Tab>
</Tab>
<Tab eventKey="repairRecords" title="Repair & Maintenance">
{tableRepair}
</Tab>
</Tabs>
<div className="list-func-panel">
{currentTab !== 'basicInfo' && <input className="me-2 with-search-icon" type="text" placeholder="Search" value={keyword} onChange={(e) => setKeyword(e.currentTarget.value)} />}
{currentTab === 'documents' && <button className="btn btn-primary" onClick={() => download()}><Download size={16} className="me-2"></Download>Download</button>}
{currentTab === 'repairRecords' && <Export
columns={columnsRepair}
data={filteredRepairs}
filename={`vehicle-${currentVehicle?.vehicle_number}-repairs`}
/>}
{(currentTab === 'documents' || currentTab === 'repairRecords') && (
<input className="me-2 with-search-icon" type="text" placeholder="Search" value={keyword} onChange={(e) => setKeyword(e.currentTarget.value)} />
)}
{currentTab === 'documents' && (
<button className="btn btn-primary" onClick={() => download()}><Download size={16} className="me-2"></Download>Download</button>
)}
{currentTab === 'repairRecords' && (
<Export
columns={columnsRepair}
data={filteredRepairs}
filename={`vehicle-${currentVehicle?.vehicle_number}-repairs`}
/>
)}
<button className="btn btn-primary ms-2" onClick={() => goToEdit(currentVehicle?.id)}><PencilSquare className="me-2" size={16}></PencilSquare>Edit</button>
</div>
</div>
</div>
@ -545,4 +546,4 @@ const tableRepair = <div className="list row mb-4">
);
};
export default ViewVehicle;
export default ViewVehicle;

View File

@ -2,7 +2,7 @@ import axios from "axios";
export default axios.create({
// baseURL: (window.location.hostname.includes('worldshine.mayo.llc') || window.location.hostname.includes('site')) ? "https://worldshine.mayo.llc/api" : ((window.location.hostname.includes('worldshine3.mayo.llc') || window.location.hostname.includes('site3')) ? "https://worldshine3.mayo.llc/api" : "https://worldshine3.mayo.llc/api"),
// for Test site only:
baseURL: (window.location.hostname.includes('ws1') ||window.location.hostname.includes('localhost') || window.location.hostname.includes('site1')) ? "https://ws1.mayosolution.com/api" : ((window.location.hostname.includes('ws3') || window.location.hostname.includes('site3')) ? "https://ws3.mayosolution.com/api" : "https://ws2.mayosolution.com/api"),
baseURL: (window.location.hostname.includes('ws1') ||window.location.hostname.includes('localhost') || window.location.hostname.includes('site1')) ? "https://ws1-tspt.mayosolution.com/api" : ((window.location.hostname.includes('ws3') || window.location.hostname.includes('site3')) ? "https://ws3.mayosolution.com/api" : "https://ws2.mayosolution.com/api"),
// baseURL: "http://localhost:8080/api",
headers: {
"Content-type": "application/json"

View File

@ -44,6 +44,10 @@ const getAvatar = (filename) => {
return http.get(`/files/${filename}`);
}
const getAvatarAsBlob = (filename) => {
return http.get(`/files/${filename}`, { responseType: 'blob' });
}
const deleteFile = (data) => {
return http.post(`/files/delete`, data);
}
@ -69,6 +73,7 @@ export const CustomerService = {
getAllActiveCustomers,
uploadAvatar,
getAvatar,
getAvatarAsBlob,
deleteFile,
createNewCustomer,
updateCustomer,

View File

@ -82,7 +82,8 @@ const getTransportationInfo = (eventsList, item, timeDocs = []) => {
}
}
const interpreterLevelOptions = [
// Language Support options
const languageSupportOptions = [
{
value: 'Checkin',
label: 'Checkin'
@ -97,22 +98,30 @@ const interpreterLevelOptions = [
},
{
value: 'Office',
label: 'Office inperson'
label: 'Office in person'
},
{
value: 'Office(Phone)',
label: 'Office byphone'
label: 'Office by phone'
},
{
value: 'Nurse',
label: 'Nurse'
}
];
const colorOptions = [
// Legacy alias for backward compatibility
const interpreterLevelOptions = languageSupportOptions;
// Label options for Medical Events
const labelOptions = [
{
value: 'red',
label: 'EyesOn'
label: 'Eyes-On'
},
{
value: 'brown',
label: 'ByOwn',
value: 'pink',
label: 'Self-Transport',
},
{
value: 'green',
@ -120,15 +129,15 @@ const colorOptions = [
},
{
value: 'blue',
label: 'Default for ByCenter'
label: 'Default for By Center'
},
{
value: 'black',
label: 'Cient Does Not Need to Go'
label: 'Medication Pickup Only'
},
{
value: 'purple',
label: 'Dropoff Only',
label: 'Drop-Off Only',
},
{
value: 'gray',
@ -136,21 +145,104 @@ const colorOptions = [
},
{
value: 'orange',
label: 'Pickup Only'
label: 'Pick-Up Only'
},
{
value: 'brown',
label: 'Translation By Center'
}
];
// Activity color options
const activityColorOptions = [
{
value: 'red',
label: 'Classes'
},
{
value: 'pink',
label: 'Translate(center)'
label: 'Games'
},
{
value: 'green',
label: 'Events'
},
{
value: 'blue',
label: 'Outings'
},
{
value: 'purple',
label: 'Personal Care'
},
{
value: 'brown',
label: 'Care Activities'
}
];
// Incident/Attendance Notes color options (always blue)
const incidentColorOptions = [
{
value: 'blue',
label: 'Attendance Note'
}
];
// Meal Plan color options
const mealPlanColorOptions = [
{
value: 'brown',
label: 'Breakfast'
},
{
value: 'green',
label: 'Lunch'
},
{
value: 'red',
label: 'Snack'
}
];
// Legacy alias for backward compatibility
const colorOptions = labelOptions;
// Transportation Support options
const transportationTypeOptions = [
{
value: 'by own',
label: 'ByOwn'
},
{
value: 'televisit',
label: 'Televisit'
},
{
value: 'client does not need to go',
label: 'Client Does Not need to Go'
},
{
value: 'dropoff only',
label: 'DropOff Only'
},
{
value: 'pickup only',
label: 'Pickup Only'
},
{
value: 'Center Transportation',
label: 'By Center Transportation'
}
];
//const baseUrl = (window.location.hostname.includes('ws1') ||window.location.hostname.includes('localhost') || window.location.hostname.includes('site1')) ? "https://ws1.mayosolution.com/api" : ((window.location.hostname.includes('ws3') || window.location.hostname.includes('site3')) ? "https://ws3.mayosolution.com/api" : "https://ws2.mayosolution.com/api")
const baseUrl = (window.location.hostname.includes('worldshine.mayo.llc') ||window.location.hostname.includes('worldshine1') || window.location.hostname.includes('site1')) ? "https://worldshine.mayo.llc/api" : ((window.location.hostname.includes('worldshine3') || window.location.hostname.includes('site3')) ? "https://worldshine3.mayo.llc/api" : "https://worldshine2.mayo.llc/api")
const baseUrl = (window.location.hostname.includes('ws1') ||window.location.hostname.includes('localhost') || window.location.hostname.includes('site1')) ? "https://ws1-tspt.mayosolution.com/api" : ((window.location.hostname.includes('ws3') || window.location.hostname.includes('site3')) ? "https://ws3.mayosolution.com/api" : "https://ws2.mayosolution.com/api")
// const baseUrl = (window.location.hostname.includes('worldshine.mayo.llc') ||window.location.hostname.includes('worldshine1') || window.location.hostname.includes('site1')) ? "https://worldshine.mayo.llc/api" : ((window.location.hostname.includes('worldshine3') || window.location.hostname.includes('site3')) ? "https://worldshine3.mayo.llc/api" : "https://worldshine2.mayo.llc/api")
//const site = (window.location.hostname.includes('ws1') ||window.location.hostname.includes('localhost') || window.location.hostname.includes('site1')) ? 1 : ((window.location.hostname.includes('ws3') || window.location.hostname.includes('site3')) ? 3 : 2)
const site = (window.location.hostname.includes('worldshine.mayo.llc') ||window.location.hostname.includes('worldshine1') || window.location.hostname.includes('site1')) ? 1 : ((window.location.hostname.includes('worldshine3') || window.location.hostname.includes('site3')) ? 3 : 2)
const site = (window.location.hostname.includes('ws1') ||window.location.hostname.includes('localhost') || window.location.hostname.includes('site1')) ? 1 : ((window.location.hostname.includes('ws3') || window.location.hostname.includes('site3')) ? 3 : 2)
//const site = (window.location.hostname.includes('worldshine.mayo.llc') ||window.location.hostname.includes('worldshine1') || window.location.hostname.includes('site1')) ? 1 : ((window.location.hostname.includes('worldshine3') || window.location.hostname.includes('site3')) ? 3 : 2)
const generatePdf = (data) => {
window.open(`${baseUrl}/docs/get-pdfs?docTemplateName=med_notification&inputData=${encodeURIComponent(JSON.stringify(data))}`);
@ -185,6 +277,14 @@ export const EventsService = {
getTimeData,
getByCustomer,
site,
// New option names
languageSupportOptions,
labelOptions,
activityColorOptions,
incidentColorOptions,
mealPlanColorOptions,
transportationTypeOptions,
// Legacy aliases for backward compatibility
interpreterLevelOptions,
colorOptions
};

View File

@ -1,4 +1,5 @@
import http from "../http-common";
const getAll = (type) => {
const params = {};
if (type) {
@ -9,10 +10,9 @@ const getAll = (type) => {
const createNewResource = (data) => {
data.status = 'active';
return http.post('/resources', data);
return http.post('/resources', data);
};
const updateResource = (id, data) => {
return http.put(`/resources/${id}`, data);
}
@ -38,7 +38,7 @@ const resourceOptionList = [
'Botox Therapy',
'Breast Surgery',
'Cardiology',
'Cardiovascular ',
'Cardiovascular',
'Colon & Rectal Surgery',
'Dentist',
'Dermatology',
@ -54,13 +54,13 @@ const resourceOptionList = [
'Head & Neck Surgery',
'Health Boutique',
'Hearing Aids',
'Hematology & Oncology',
'Hepatology',
'Hematology & Oncology',
'Hepatology',
'Hospital',
'Infectious disease',
'Medical Center',
'Lab',
'Modified Barium Swallow (MBS) Study ',
'Modified Barium Swallow (MBS) Study',
'Medical Supply',
'Nephrology',
'Neuro Surgeon',
@ -94,7 +94,7 @@ const resourceOptionList = [
'Thoracic Surgery',
'Traditional Chinese Medicine',
'Urgent Care',
'Urogynecology',
'Urogynecology',
'Urology',
'Vascular and Vein',
'Vascular & Interventional Radiologist',

View File

@ -19,6 +19,7 @@ export * from './LabelService';
export * from './SeatingService';
export * from './AttendanceNoteService';
export * from './CarouselService';
export * from './DailyRoutesTemplateService';
// Utility functions
export const parseDateFromBackend = (dateString) => {

View File

@ -2,7 +2,7 @@ import React, { useState } from "react";
import { Dropdown } from "react-bootstrap";
import { Download } from "react-bootstrap-icons";
const Export = ({ columns, data, filename = "export" }) => {
const Export = ({ columns, data, filename = "export", customActions = [] }) => {
const [showExportDropdown, setShowExportDropdown] = useState(false);
const [exportColumns, setExportColumns] = useState(
columns.map(col => ({ ...col, show: true }))
@ -117,6 +117,21 @@ const Export = ({ columns, data, filename = "export" }) => {
<h6>Export Options</h6>
<div className="app-main-content-fields-section margin-sm dropdown-container">
<div className="me-4">
{customActions.length > 0 && (
<>
{customActions.map((action, index) => (
<button
key={index}
className="btn btn-primary btn-sm mb-2"
style={{ width: '100%' }}
onClick={action.onClick}
>
{action.label}
</button>
))}
<hr style={{ margin: '8px 0', borderColor: '#ddd' }} />
</>
)}
<div style={{ maxHeight: '200px', overflowY: 'auto', marginBottom: '15px' }}>
<h6 style={{ fontSize: '14px', marginBottom: '10px' }}>Select Columns:</h6>
{exportColumns.map((column) => (

View File

@ -14,24 +14,247 @@ export const PICKUP_STATUS_TEXT = {
export const CUSTOMER_TYPE = {
MEMBER: 'member',
VOLUNTEER: 'volunteer',
SELF_PAY: 'selfPayMember',
VISITOR: 'visitor',
TRANSFERRED: 'transferred',
DISCHARED: 'discharged',
DECEASED: 'deceased'
VISITOR: 'visitor'
}
export const CUSTOMER_TYPE_TEXT = {
member: 'Member',
volunteer: 'Volunteer',
selfPayMember: 'Self-Pay Member',
visitor: 'Visitor',
transferred: 'Transferred',
discharged: 'Discharged',
deceased: 'Deceased'
deceased: 'Deceased',
discharged: 'Discharged'
}
export const PROGRAM_TYPE = {
AMDC: 'amdc',
SENIOR_PLUS: 'seniorPlus'
}
export const PROGRAM_TYPE_TEXT = {
amdc: 'AMDC',
seniorPlus: 'Senior Plus'
}
export const PAY_SOURCE = {
PRIVATE_PAY: 'privatePay',
MEDICAID: 'medicaid',
QMB: 'qmb',
VETERAN: 'veteran',
OTHER: 'other'
}
export const PAY_SOURCE_TEXT = {
privatePay: 'Private Pay',
medicaid: 'Medicaid',
qmb: 'QMB',
veteran: 'Veteran',
other: 'Other-Please Specify'
}
export const LEGAL_SEX = {
FEMALE: 'female',
MALE: 'male'
}
export const LEGAL_SEX_TEXT = {
female: 'Female',
male: 'Male'
}
export const MARITAL_STATUS = {
MARRIED: 'married',
WIDOWED: 'widowed',
SINGLE: 'single',
DIVORCED: 'divorced'
}
export const MARITAL_STATUS_TEXT = {
married: 'Married',
widowed: 'Widowed',
single: 'Single',
divorced: 'Divorced'
}
export const IMMIGRATION_STATUS = {
US_CITIZEN: 'usCitizen',
GREEN_CARD_HOLDER: 'greenCardHolder',
PERMANENT_RESIDENT: 'permanentResident',
OTHER: 'other'
}
export const IMMIGRATION_STATUS_TEXT = {
usCitizen: 'U.S. Citizen',
greenCardHolder: 'Green Card Holder',
permanentResident: 'Permanent Resident',
other: 'Other-Please Specify'
}
export const LANGUAGE_OPTIONS = [
{ value: 'english', label: 'English' },
{ value: 'mandarin', label: 'Mandarin' },
{ value: 'cantonese', label: 'Cantonese' },
{ value: 'vietnamese', label: 'Vietnamese' },
{ value: 'korean', label: 'Korean' },
{ value: 'thai', label: 'Thai' },
{ value: 'burmese', label: 'Burmese' },
{ value: 'french', label: 'French' },
{ value: 'haitianCreole', label: 'Haitian Creole' },
{ value: 'hindi', label: 'Hindi' },
{ value: 'persian', label: 'Persian' },
{ value: 'spanish', label: 'Spanish' },
{ value: 'urdu', label: 'Urdu' },
{ value: 'other', label: 'Other-Please Specify' }
]
export const STATE_OPTIONS = {
MARYLAND: 'maryland',
VIRGINIA: 'virginia'
}
export const STATE_OPTIONS_TEXT = {
maryland: 'Maryland',
virginia: 'Virginia'
}
export const EMERGENCY_CONTACT_RELATIONSHIP = {
SON: 'son',
DAUGHTER: 'daughter',
SPOUSE: 'spouse',
FRIEND: 'friend',
CAREGIVER: 'caregiver',
OTHER: 'other'
}
export const EMERGENCY_CONTACT_RELATIONSHIP_TEXT = {
son: 'Son',
daughter: 'Daughter',
spouse: 'Spouse',
friend: 'Friend',
caregiver: 'Caregiver',
other: 'Other-Please Specify'
}
export const EMERGENCY_CONTACT_ROLE_OPTIONS = [
{ value: 'nextOfKin', label: 'Next of Kin' },
{ value: 'emergencyContact', label: 'Emergency contact' },
{ value: 'payor', label: 'Payor' },
{ value: 'primaryCaregiver', label: 'Primary Caregiver' },
{ value: 'careProvider', label: 'Care Provider' },
{ value: 'caseManager', label: 'Case Manager' },
{ value: 'powerOfAttorney', label: 'Power of Attorney' },
{ value: 'serviceCoordinator', label: 'Service Coordinator' },
{ value: 'friend', label: 'Friend' },
{ value: 'guardianOrPoaForHealthcare', label: 'Guardian or PoA for Healthcare' },
{ value: 'healthcareProxy', label: 'Healthcare Proxy' }
]
export const DAYS_OF_WEEK_OPTIONS = [
{ value: 'monday', label: 'Monday' },
{ value: 'tuesday', label: 'Tuesday' },
{ value: 'wednesday', label: 'Wednesday' },
{ value: 'thursday', label: 'Thursday' },
{ value: 'friday', label: 'Friday' },
{ value: 'saturday', label: 'Saturday' },
{ value: 'sunday', label: 'Sunday' }
]
// Referral Source - Single Choice with categories
export const REFERRAL_SOURCE = {
// Returning
TRANSFER_WORLDSHINE: 'transferWorldshine',
TRANSFER_AMDC: 'transferAmdc',
REJOIN: 'rejoin',
// Referral
FAMILY_FRIENDS: 'familyFriends',
HEALTHCARE_PROVIDER: 'healthcareProvider',
SOCIAL_WORKER: 'socialWorker',
SUPPORT_PLANNER: 'supportPlanner',
// Online & Media Source
ONLINE_SEARCH: 'onlineSearch',
FACEBOOK: 'facebook',
INSTAGRAM: 'instagram',
REDNOTE: 'rednote',
YOUTUBE: 'youtube',
WECHAT: 'wechat',
NEWSPAPER: 'newspaper',
NEXTDOOR: 'nextdoor',
// Event
EVENT: 'event',
// N/A
NA: 'na',
// Other
OTHER: 'other'
}
export const REFERRAL_SOURCE_TEXT = {
// Returning
transferWorldshine: 'Transfer from Another Worldshine Center',
transferAmdc: 'Transfer from Another AMDC Center',
rejoin: 'Rejoin',
// Referral
familyFriends: 'Family and Friends',
healthcareProvider: 'Healthcare Provider Referral',
socialWorker: 'Social Worker Referral',
supportPlanner: 'Support Planner',
// Online & Media Source
onlineSearch: 'Online Search',
facebook: 'Facebook',
instagram: 'Instagram',
rednote: 'RedNote',
youtube: 'YouTube',
wechat: 'WeChat Official Account',
newspaper: 'Newspaper',
nextdoor: 'Nextdoor',
// Event
event: 'Event',
// N/A
na: 'N/A',
// Other
other: 'Other-Please Specify'
}
// Referral Source grouped options for display
export const REFERRAL_SOURCE_GROUPED = [
{
label: 'Returning',
options: [
{ value: 'transferWorldshine', label: 'Transfer from Another Worldshine Center' },
{ value: 'transferAmdc', label: 'Transfer from Another AMDC Center' },
{ value: 'rejoin', label: 'Rejoin' }
]
},
{
label: 'Referral',
options: [
{ value: 'familyFriends', label: 'Family and Friends' },
{ value: 'healthcareProvider', label: 'Healthcare Provider Referral' },
{ value: 'socialWorker', label: 'Social Worker Referral' },
{ value: 'supportPlanner', label: 'Support Planner' }
]
},
{
label: 'Online & Media Source',
options: [
{ value: 'onlineSearch', label: 'Online Search' },
{ value: 'facebook', label: 'Facebook' },
{ value: 'instagram', label: 'Instagram' },
{ value: 'rednote', label: 'RedNote' },
{ value: 'youtube', label: 'YouTube' },
{ value: 'wechat', label: 'WeChat Official Account' },
{ value: 'newspaper', label: 'Newspaper' },
{ value: 'nextdoor', label: 'Nextdoor' }
]
},
{
label: 'Other',
options: [
{ value: 'event', label: 'Event' },
{ value: 'na', label: 'N/A' },
{ value: 'other', label: 'Other-Please Specify' }
]
}
]
export const CUSTOMER_JOIN_REASON = {
FRIEND_FAMILY_REFERRAL: 'friendFamilyReferral',
SOCIAL_WORKER_REFERRAL: 'socialWorkerReferral',
@ -50,16 +273,180 @@ export const CUSTOMER_JOIN_REASON_TEXT = {
export const CUSTOMER_DISCHARGE_REASON = {
ABSENT_OVER_30: 'absentOver30',
TRANSFERRED_TO_ASSISTED_LIVING: 'TransferredToAssignedLiving',
DECEASED: 'Deceased',
EVENT: 'Event',
OTHER: 'Other'
TRANSFER_WORLDSHINE: 'transferWorldshine',
TRANSFER_AMDC: 'transferAmdc',
TRANSFER_OTHER_FACILITY: 'transferOtherFacility',
DECEASED: 'deceased',
HOSPITALIZED: 'hospitalized',
MOVING_OUT_SERVICE_AREA: 'movingOutServiceArea',
NO_LONGER_ELIGIBLE: 'noLongerEligible',
NA: 'na',
OTHER: 'other'
}
export const CUSTOMER_DISCHARGE_REASON_TEXT = {
absentOver30: 'Absent for Over 30 Days',
TransferredToAssignedLiving: 'Transferred to Assisted Living',
Deceased: 'Deceased',
Event: 'Event',
Other: 'Other'
}
transferWorldshine: 'Transfer to Another Worldshine Center',
transferAmdc: 'Transfer to Another AMDC Center',
transferOtherFacility: 'Transfer to Another Type of Facility',
deceased: 'Deceased',
hospitalized: 'Hospitalized',
movingOutServiceArea: 'Moving out of the Service Area',
noLongerEligible: 'No Longer Eligible for ADS',
na: 'N/A',
other: 'Other-Please Specify'
}
// Dietary Restrictions - Multi Choice with categories
export const DIETARY_RESTRICTIONS_OPTIONS = [
// Regular
{ value: 'regular', label: 'Regular' },
// Meat
{ value: 'noRedMeat', label: 'No Red Meat' },
{ value: 'noPork', label: 'No Pork' },
{ value: 'noBeef', label: 'No Beef' },
{ value: 'noLamb', label: 'No Lamb' },
{ value: 'noChicken', label: 'No Chicken' },
{ value: 'noTurkey', label: 'No Turkey' },
{ value: 'noDuck', label: 'No Duck' },
// Seafood
{ value: 'noFish', label: 'No Fish' },
{ value: 'noSeafood', label: 'No Seafood' },
{ value: 'noShrimp', label: 'No Shrimp' },
{ value: 'noShellfish', label: 'No Shellfish' },
// Dairy
{ value: 'noDairy', label: 'No Dairy' },
{ value: 'noLactose', label: 'No Lactose' },
{ value: 'noYoghurt', label: 'No Yoghurt' },
{ value: 'noBoiledEgg', label: 'No Boiled Egg' },
{ value: 'noEgg', label: 'No Egg' },
// Nuts and Snacks
{ value: 'noNuts', label: 'No Nuts' },
{ value: 'noPeanuts', label: 'No Peanuts' },
{ value: 'noCrackers', label: 'No Crackers' },
// Vegetables and Fruits
{ value: 'noVegetables', label: 'No Vegetables' },
{ value: 'noFruits', label: 'No Fruits' },
{ value: 'noOrange', label: 'No Orange' },
{ value: 'noApple', label: 'No Apple' },
{ value: 'noGrapefruit', label: 'No Grapefruit' },
{ value: 'noGrapes', label: 'No Grapes' },
{ value: 'noCucumber', label: 'No Cucumber' },
{ value: 'noCorn', label: 'No Corn' },
{ value: 'noPickles', label: 'No Pickles' },
{ value: 'noEggplants', label: 'No Eggplants' },
{ value: 'noLettuce', label: 'No Lettuce' }
]
// Dietary Restrictions grouped options for display
export const DIETARY_RESTRICTIONS_GROUPED = [
{
label: 'Regular',
options: [
{ value: 'regular', label: 'Regular' }
]
},
{
label: 'Meat',
options: [
{ value: 'noRedMeat', label: 'No Red Meat' },
{ value: 'noPork', label: 'No Pork' },
{ value: 'noBeef', label: 'No Beef' },
{ value: 'noLamb', label: 'No Lamb' },
{ value: 'noChicken', label: 'No Chicken' },
{ value: 'noTurkey', label: 'No Turkey' },
{ value: 'noDuck', label: 'No Duck' }
]
},
{
label: 'Seafood',
options: [
{ value: 'noFish', label: 'No Fish' },
{ value: 'noSeafood', label: 'No Seafood' },
{ value: 'noShrimp', label: 'No Shrimp' },
{ value: 'noShellfish', label: 'No Shellfish' }
]
},
{
label: 'Dairy',
options: [
{ value: 'noDairy', label: 'No Dairy' },
{ value: 'noLactose', label: 'No Lactose' },
{ value: 'noYoghurt', label: 'No Yoghurt' },
{ value: 'noBoiledEgg', label: 'No Boiled Egg' },
{ value: 'noEgg', label: 'No Egg' }
]
},
{
label: 'Nuts and Snacks',
options: [
{ value: 'noNuts', label: 'No Nuts' },
{ value: 'noPeanuts', label: 'No Peanuts' },
{ value: 'noCrackers', label: 'No Crackers' }
]
},
{
label: 'Vegetables and Fruits',
options: [
{ value: 'noVegetables', label: 'No Vegetables' },
{ value: 'noFruits', label: 'No Fruits' },
{ value: 'noOrange', label: 'No Orange' },
{ value: 'noApple', label: 'No Apple' },
{ value: 'noGrapefruit', label: 'No Grapefruit' },
{ value: 'noGrapes', label: 'No Grapes' },
{ value: 'noCucumber', label: 'No Cucumber' },
{ value: 'noCorn', label: 'No Corn' },
{ value: 'noPickles', label: 'No Pickles' },
{ value: 'noEggplants', label: 'No Eggplants' },
{ value: 'noLettuce', label: 'No Lettuce' }
]
}
]
export const DIET_TEXTURE = {
REGULAR: 'regular',
CHOPPED: 'chopped',
PUREED: 'pureed',
LIQUID: 'liquid'
}
export const DIET_TEXTURE_TEXT = {
regular: 'Regular',
chopped: 'Chopped',
pureed: 'Pureed',
liquid: 'Liquid'
}
export const TRANSPORTATION_TYPE = {
ROUND_TRIP: 'roundTrip',
PICKUP_ONLY: 'pickupOnly',
DROPOFF_ONLY: 'dropoffOnly',
SELF_TRANSPORT: 'selfTransport'
}
export const TRANSPORTATION_TYPE_TEXT = {
roundTrip: 'Round Trip',
pickupOnly: 'Pick-up Only',
dropoffOnly: 'Drop-off Only',
selfTransport: 'Self-Transport'
}
export const YES_NO = {
YES: 'yes',
NO: 'no'
}
export const YES_NO_TEXT = {
yes: 'Yes',
no: 'No'
}
export const PREFERRED_TEXT_LANGUAGE = {
ENGLISH: 'english',
CHINESE: 'chinese'
}
export const PREFERRED_TEXT_LANGUAGE_TEXT = {
english: 'English',
chinese: 'Chinese'
}

View File

@ -1,4 +1,6 @@
export * from "./customer.constant";
export * from "./route-status.constant";
export * from "./employee.constant";
export * from "./report.constant";
export * from "./report.constant";
export * from "./vehicle.constant";
export * from "./resource.constant";

View File

@ -12,10 +12,10 @@ const employeeUploadBasePath = '/www/wwwroot/upload/';
app.use('/files', express.static(employeeUploadBasePath));
app.use(cors(corsOptions));
// parse requests of content-type - application/json
app.use(bodyParser.json());
app.use(bodyParser.json({ limit: '50mb' }));
app.use(express.static(path));
// parse requests of content-type - application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
const db = require('./app/models');
db.mongoose
.connect(db.url, {
@ -72,6 +72,18 @@ app.get('/trans-routes/route-signature', function (req,res) {
app.get('/trans-routes/templates/edit/:id', function (req,res) {
res.sendFile(path + "index.html");
});
app.get('/trans-routes/daily-templates/list', function (req,res) {
res.sendFile(path + "index.html");
});
app.get('/trans-routes/daily-templates/view/:id', function (req,res) {
res.sendFile(path + "index.html");
});
app.get('/trans-routes/daily-templates/:id/view-route/:routeId', function (req,res) {
res.sendFile(path + "index.html");
});
app.get('/trans-routes/daily-templates/:id/update-route/:routeId', function (req,res) {
res.sendFile(path + "index.html");
});
app.get('/employees', function (req,res) {
res.sendFile(path + "index.html");
});
@ -201,6 +213,9 @@ app.get('/seating', function (req,res) {
app.get('/center-calendar', function (req,res) {
res.sendFile(path + "index.html");
});
app.get('/meal-status', function (req,res) {
res.sendFile(path + "index.html");
});
app.get('/info-screen', function (req,res) {
res.sendFile(path + "index.html");
});
@ -233,6 +248,8 @@ require("./app/routes/seating.routes")(app);
require("./app/routes/label.routes")(app);
require("./app/routes/attendance-note.routes")(app);
require("./app/routes/carousel.routes")(app);
require("./app/routes/fingerprint-attendance.routes")(app);
require("./app/routes/daily-routes-template.routes")(app);
require("./app/scheduler/reminderScheduler");