multi select box

This commit is contained in:
2026-02-23 11:26:47 -05:00
parent 0a94295c6b
commit da64fd6e2c
19 changed files with 338 additions and 53 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
app/.DS_Store vendored

Binary file not shown.

BIN
app/views/.DS_Store vendored

Binary file not shown.

View File

@@ -1,16 +1,16 @@
{
"files": {
"main.css": "/static/css/main.295faf6a.css",
"main.js": "/static/js/main.8e7190e1.js",
"main.css": "/static/css/main.3078ab9c.css",
"main.js": "/static/js/main.d4629ce4.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.295faf6a.css.map": "/static/css/main.295faf6a.css.map",
"main.8e7190e1.js.map": "/static/js/main.8e7190e1.js.map",
"main.3078ab9c.css.map": "/static/css/main.3078ab9c.css.map",
"main.d4629ce4.js.map": "/static/js/main.d4629ce4.js.map",
"787.c4e7f8f9.chunk.js.map": "/static/js/787.c4e7f8f9.chunk.js.map"
},
"entrypoints": [
"static/css/main.295faf6a.css",
"static/js/main.8e7190e1.js"
"static/css/main.3078ab9c.css",
"static/js/main.d4629ce4.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.8e7190e1.js"></script><link href="/static/css/main.295faf6a.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.d4629ce4.js"></script><link href="/static/css/main.3078ab9c.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

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

BIN
client/.DS_Store vendored

Binary file not shown.

View File

@@ -2477,4 +2477,167 @@ input[type="checkbox"] {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* MultiSelectDropdown */
.multi-select-dropdown {
position: relative;
width: 300px;
}
.multi-select-dropdown__control {
display: flex;
align-items: flex-start;
justify-content: space-between;
min-height: 45px;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 8px;
background: #fff;
cursor: pointer;
transition: border-color 0.2s;
width: 100%;
box-sizing: border-box;
}
.multi-select-dropdown__control:hover {
border-color: #999;
}
.multi-select-dropdown__control--open {
border-color: #2684FF;
box-shadow: 0 0 0 1px #2684FF;
}
.multi-select-dropdown__value-container {
flex: 1;
min-width: 0;
overflow: hidden;
}
.multi-select-dropdown__placeholder {
color: #999;
font-size: 14px;
line-height: 32px;
}
.multi-select-dropdown__tags {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
}
.multi-select-dropdown__tag {
display: inline-flex;
align-items: center;
background: #E8F0FE;
color: #1A73E8;
border-radius: 4px;
padding: 2px 6px;
font-size: 12px;
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.multi-select-dropdown__tag-remove {
margin-left: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
color: #1A73E8;
flex-shrink: 0;
line-height: 1;
}
.multi-select-dropdown__tag-remove:hover {
color: #c00;
}
.multi-select-dropdown__indicator {
display: flex;
align-items: center;
padding-left: 8px;
padding-top: 8px;
flex-shrink: 0;
}
.multi-select-dropdown__menu {
position: absolute;
top: calc(100% + 4px);
left: 0;
width: 100%;
z-index: 999;
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
box-sizing: border-box;
overflow: hidden;
}
.multi-select-dropdown__search-container {
padding: 10px 14px;
border-bottom: 1px solid #eee;
box-sizing: border-box;
}
.multi-select-dropdown__search {
display: block;
width: 100% !important;
height: auto !important;
padding: 8px 10px !important;
border: 1px solid #ddd !important;
border-radius: 6px !important;
font-size: 14px;
outline: none;
box-sizing: border-box !important;
}
.multi-select-dropdown__search:focus {
border-color: #2684FF !important;
}
.multi-select-dropdown__select-all {
border-bottom: 1px solid #eee;
font-weight: 600;
}
.multi-select-dropdown__options-list {
max-height: 220px;
overflow-y: auto;
}
.multi-select-dropdown__option {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
cursor: pointer;
font-size: 14px;
transition: background 0.15s;
}
.multi-select-dropdown__option:hover {
background: #f5f5f5;
}
.multi-select-dropdown__option--selected {
background: #EBF5FF;
}
.multi-select-dropdown__checkbox {
width: 18px;
height: 18px;
accent-color: #2684FF;
cursor: pointer;
flex-shrink: 0;
}
.multi-select-dropdown__no-options {
padding: 16px;
text-align: center;
color: #999;
font-size: 14px;
}

View File

@@ -26,7 +26,8 @@ import {
DIET_TEXTURE, DIET_TEXTURE_TEXT,
TRANSPORTATION_TYPE, TRANSPORTATION_TYPE_TEXT,
YES_NO, YES_NO_TEXT,
PREFERRED_TEXT_LANGUAGE, PREFERRED_TEXT_LANGUAGE_TEXT
PREFERRED_TEXT_LANGUAGE, PREFERRED_TEXT_LANGUAGE_TEXT,
MultiSelectDropdown
} from "../../shared";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab } from "react-bootstrap";
import { Upload } from "react-bootstrap-icons";
@@ -684,12 +685,10 @@ const CreateCustomer = () => {
)}
<div className="me-4 mb-3">
<div className="field-label">Language Spoken</div>
<Select
isMulti
<MultiSelectDropdown
value={languageSpoken}
onChange={setLanguageSpoken}
options={LANGUAGE_OPTIONS}
styles={multiSelectStyles}
placeholder="e.g., English, Mandarin"
/>
</div>
@@ -820,12 +819,10 @@ const CreateCustomer = () => {
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Emergency Contact Role</div>
<Select
isMulti
<MultiSelectDropdown
value={contact.role}
onChange={(value) => updateEmergencyContact(index, 'role', value)}
options={EMERGENCY_CONTACT_ROLE_OPTIONS}
styles={multiSelectStyles}
placeholder="e.g., Power of Attorney"
/>
</div>
@@ -843,12 +840,10 @@ const CreateCustomer = () => {
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Days of Week</div>
<Select
isMulti
<MultiSelectDropdown
value={daysOfWeek}
onChange={setDaysOfWeek}
options={DAYS_OF_WEEK_OPTIONS}
styles={multiSelectStyles}
placeholder="e.g., Mon, Wed, Fri"
/>
</div>
@@ -937,12 +932,10 @@ const CreateCustomer = () => {
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Dietary Restrictions</div>
<Select
isMulti
<MultiSelectDropdown
value={dietaryRestrictions}
onChange={setDietaryRestrictions}
options={DIETARY_RESTRICTIONS_GROUPED}
styles={multiSelectStyles}
placeholder="e.g., No Pork"
/>
</div>

View File

@@ -27,7 +27,8 @@ import {
DIET_TEXTURE, DIET_TEXTURE_TEXT,
TRANSPORTATION_TYPE, TRANSPORTATION_TYPE_TEXT,
YES_NO, YES_NO_TEXT,
PREFERRED_TEXT_LANGUAGE, PREFERRED_TEXT_LANGUAGE_TEXT
PREFERRED_TEXT_LANGUAGE, PREFERRED_TEXT_LANGUAGE_TEXT,
MultiSelectDropdown
} from "../../shared";
import { Upload, BoxArrowRight, CheckCircleFill } from "react-bootstrap-icons";
@@ -1018,12 +1019,10 @@ const UpdateCustomer = () => {
)}
<div className="me-4 mb-3">
<div className="field-label">Language Spoken</div>
<Select
isMulti
<MultiSelectDropdown
value={languageSpoken}
onChange={setLanguageSpoken}
options={LANGUAGE_OPTIONS}
styles={multiSelectStyles}
placeholder="e.g., English, Mandarin"
/>
</div>
@@ -1154,12 +1153,10 @@ const UpdateCustomer = () => {
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Emergency Contact Role</div>
<Select
isMulti
<MultiSelectDropdown
value={contact.role}
onChange={(value) => updateEmergencyContact(index, 'role', value)}
options={EMERGENCY_CONTACT_ROLE_OPTIONS}
styles={multiSelectStyles}
placeholder="e.g., Power of Attorney"
/>
</div>
@@ -1177,12 +1174,10 @@ const UpdateCustomer = () => {
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Days of Week</div>
<Select
isMulti
<MultiSelectDropdown
value={daysOfWeek}
onChange={setDaysOfWeek}
options={DAYS_OF_WEEK_OPTIONS}
styles={multiSelectStyles}
placeholder="e.g., Mon, Wed, Fri"
/>
</div>
@@ -1286,12 +1281,10 @@ const UpdateCustomer = () => {
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Dietary Restrictions</div>
<Select
isMulti
<MultiSelectDropdown
value={dietaryRestrictions}
onChange={setDietaryRestrictions}
options={DIETARY_RESTRICTIONS_GROUPED}
styles={multiSelectStyles}
placeholder="e.g., No Pork"
/>
</div>

View File

@@ -4,6 +4,7 @@ import { AuthService, MessageService, CustomerService } from "../../services";
import { Breadcrumb, Tabs, Tab, Modal, Button, Dropdown } from "react-bootstrap";
import { PencilSquare, Plus, Trash, Send, Eye, ArrowLeft, Filter, Calendar3, Search, ChevronLeft, ChevronRight } from "react-bootstrap-icons";
import Select from 'react-select';
import { MultiSelectDropdown } from '../../shared';
import DatePicker from 'react-datepicker';
import moment from 'moment';
import 'react-datepicker/dist/react-datepicker.css';
@@ -520,22 +521,11 @@ const MessageList = () => {
<Modal.Body>
<div className="mb-3">
<label className="form-label"><strong>Select Recipient</strong> <span className="text-danger">*</span></label>
<div className="d-flex align-items-center mb-2">
<button className="btn btn-outline-secondary btn-sm" onClick={handleSelectAll}>
{selectedRecipients.length === eligibleCustomers.length ? 'Deselect All' : 'Select All'}
</button>
<span className="ms-2 text-muted" style={{ fontSize: '13px' }}>{selectedRecipients.length} selected</span>
</div>
<Select
isMulti
<MultiSelectDropdown
value={selectedRecipients}
onChange={(val) => setSelectedRecipients(val || [])}
options={eligibleCustomers.map(c => ({ value: c.id, label: c.name }))}
placeholder="e.g., Recipient Name"
styles={{
control: (base) => ({ ...base, borderRadius: '8px' }),
indicatorSeparator: () => ({ display: 'none' })
}}
/>
</div>
<div className="mb-3">

View File

@@ -0,0 +1,145 @@
import React, { useState, useRef, useEffect } from 'react';
const MultiSelectDropdown = ({
value = [],
onChange,
options = [],
placeholder = 'Select...',
}) => {
const [isOpen, setIsOpen] = useState(false);
const [search, setSearch] = useState('');
const containerRef = useRef(null);
useEffect(() => {
const handleClickOutside = (e) => {
if (containerRef.current && !containerRef.current.contains(e.target)) {
setIsOpen(false);
setSearch('');
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// Flatten grouped options into a flat list for filtering/selection
const flatOptions = options.reduce((acc, opt) => {
if (opt.options) {
return [...acc, ...opt.options];
}
return [...acc, opt];
}, []);
const selectedValues = (value || []).map(v => v.value);
const filteredOptions = search
? flatOptions.filter(opt => opt.label.toLowerCase().includes(search.toLowerCase()))
: flatOptions;
const allFilteredSelected = filteredOptions.length > 0 && filteredOptions.every(opt => selectedValues.includes(opt.value));
const toggleOption = (opt) => {
const exists = selectedValues.includes(opt.value);
if (exists) {
onChange(value.filter(v => v.value !== opt.value));
} else {
onChange([...value, opt]);
}
};
const toggleSelectAll = () => {
if (allFilteredSelected) {
const filteredValues = filteredOptions.map(o => o.value);
onChange(value.filter(v => !filteredValues.includes(v.value)));
} else {
const currentValues = new Set(selectedValues);
const newItems = filteredOptions.filter(o => !currentValues.has(o.value));
onChange([...value, ...newItems]);
}
};
const removeItem = (e, opt) => {
e.stopPropagation();
onChange(value.filter(v => v.value !== opt.value));
};
return (
<div className="multi-select-dropdown" ref={containerRef}>
<div
className={`multi-select-dropdown__control ${isOpen ? 'multi-select-dropdown__control--open' : ''}`}
onClick={() => setIsOpen(!isOpen)}
>
<div className="multi-select-dropdown__value-container">
{value.length === 0 ? (
<span className="multi-select-dropdown__placeholder">{placeholder}</span>
) : (
<div className="multi-select-dropdown__tags">
{value.map((v) => (
<span key={v.value} className="multi-select-dropdown__tag">
{v.label}
<span className="multi-select-dropdown__tag-remove" onClick={(e) => removeItem(e, v)}>&times;</span>
</span>
))}
</div>
)}
</div>
<div className="multi-select-dropdown__indicator">
<svg width="16" height="16" viewBox="0 0 20 20" style={{ transform: isOpen ? 'rotate(180deg)' : 'none', transition: 'transform 0.2s' }}>
<path d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z" fill="#999" />
</svg>
</div>
</div>
{isOpen && (
<div className="multi-select-dropdown__menu">
<div className="multi-select-dropdown__search-container">
<input
type="text"
className="multi-select-dropdown__search"
placeholder="Search..."
value={search}
onChange={(e) => setSearch(e.target.value)}
onClick={(e) => e.stopPropagation()}
autoFocus
/>
</div>
<div
className="multi-select-dropdown__option multi-select-dropdown__select-all"
onClick={toggleSelectAll}
>
<input
type="checkbox"
checked={allFilteredSelected}
onChange={toggleSelectAll}
onClick={(e) => e.stopPropagation()}
className="multi-select-dropdown__checkbox"
/>
<span>Select All</span>
</div>
<div className="multi-select-dropdown__options-list">
{filteredOptions.length === 0 && (
<div className="multi-select-dropdown__no-options">No options found</div>
)}
{filteredOptions.map((opt) => (
<div
key={opt.value}
className={`multi-select-dropdown__option ${selectedValues.includes(opt.value) ? 'multi-select-dropdown__option--selected' : ''}`}
onClick={() => toggleOption(opt)}
>
<input
type="checkbox"
checked={selectedValues.includes(opt.value)}
onChange={() => toggleOption(opt)}
onClick={(e) => e.stopPropagation()}
className="multi-select-dropdown__checkbox"
/>
<span>{opt.label}</span>
</div>
))}
</div>
</div>
)}
</div>
);
};
export default MultiSelectDropdown;

View File

@@ -1,2 +1,3 @@
export { default as ManageTable } from './ManageTable';
export { default as Export } from './Export';
export { default as Export } from './Export';
export { default as MultiSelectDropdown } from './MultiSelectDropdown';