PluginProbe
Tiered Pricing Table for WooCommerce / 6.4.0
Tiered Pricing Table for WooCommerce v6.4.0
8.0.2 7.1.7 7.1.5 7.1.4 7.1.1 6.5.0 6.4.0 6.1.0 trunk 1.0 1.1 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.2.0 2.2.1 2.2.2 2.2.3 2.3.0 2.3.1 2.3.2 2.3.3 All 91 releases
tier-pricing-table / src / Addons / TaxSettings / js-source / App.js

App.js in Tiered Pricing Table for WooCommerce 6.4.0, at src/Addons/TaxSettings/js-source/App.js

404 lines 12.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import {useState, useEffect} from '@wordpress/element';
2 import {
3 SelectControl,
4 RadioControl,
5 TextControl,
6 ToggleControl,
7 Button,
8 Spinner,
9 Notice,
10 Card,
11 CardHeader,
12 CardBody,
13 Placeholder,
14 Flex,
15 FlexItem,
16 FlexBlock,
17 Panel,
18 PanelBody,
19 PanelRow,
20 Icon
21 } from '@wordpress/components';
22 import {trash, plus} from '@wordpress/icons';
23 import apiFetch from '@wordpress/api-fetch';
24 import {__} from '@wordpress/i18n';
25
26 const ROLES = window.tptTaxSettings?.roles || [
27 {label: 'Administrator', value: 'administrator'},
28 {label: 'Editor', value: 'editor'},
29 {label: 'Author', value: 'author'},
30 {label: 'Contributor', value: 'contributor'},
31 {label: 'Subscriber', value: 'subscriber'},
32 {label: 'Customer', value: 'customer'},
33 {label: 'Shop manager', value: 'shop_manager'}
34 ];
35
36 export default function App() {
37 const [settings, setSettings] = useState(null);
38 const [loading, setLoading] = useState(true);
39 const [activeRoles, setActiveRoles] = useState([]);
40 const [newRole, setNewRole] = useState('');
41 const [newlyAddedRoles, setNewlyAddedRoles] = useState([]);
42
43 useEffect(() => {
44 apiFetch({path: '/tier-pricing-table/v1/tax-settings'}).then((response) => {
45 setSettings(response || {});
46 setActiveRoles(Object.keys(response || {}));
47 setLoading(false);
48 }).catch(() => {
49 setSettings({});
50 setLoading(false);
51 });
52 }, []);
53
54
55 const addRole = () => {
56 if (!newRole || activeRoles.includes(newRole)) return;
57
58 setActiveRoles([...activeRoles, newRole]);
59 setNewlyAddedRoles([...newlyAddedRoles, newRole]);
60 setSettings({
61 ...settings,
62 [newRole]: {
63 tax_exempt: false,
64 tax_class: 'default',
65 display_shop: 'default',
66 display_cart: 'default',
67 prices_include_tax: 'default',
68 price_suffix: ''
69 }
70 });
71 setNewRole('');
72 };
73
74 const removeRole = (role) => {
75 const newRoles = activeRoles.filter(r => r !== role);
76 const newSettings = {...settings};
77 delete newSettings[role];
78
79 setActiveRoles(newRoles);
80 setSettings(newSettings);
81 };
82
83 const updateRoleSetting = (role, key, value) => {
84 setSettings({
85 ...settings,
86 [role]: {
87 ...settings[role],
88 [key]: value
89 }
90 });
91 };
92
93 if (loading) {
94 return <Spinner/>;
95 }
96
97 const availableRolesToAdd = ROLES.filter(r => !activeRoles.includes(r.value));
98
99 return (
100 <div className="tpt-tax-settings-app">
101 <style>
102 {`
103 .tpt-tax-settings-app {
104 margin-top: 20px;
105 max-width: 650px;
106 }
107 .tpt-role-settings-panel {
108 border: 1px solid var(--wp-components-color-border, #c3c4c7);
109 background: var(--wp-components-color-background, #fff);
110 margin-bottom: 20px;
111 border-radius: 4px;
112 box-shadow: 0 1px 1px rgba(0,0,0,.04);
113 }
114 .tpt-role-settings-panel > .components-button {
115 background: var(--wp-admin-theme-color-darker-20, #f6f7f7);
116 border-bottom: 1px solid var(--wp-components-color-border, #c3c4c7);
117 font-weight: 600;
118 color: var(--wp-components-color-foreground, #1d2327);
119 }
120 .tpt-role-settings-panel > .components-button:hover {
121 background: var(--wp-admin-theme-color-darker-10, #f0f0f1);
122 }
123 .tpt-premium-banner {
124 background: #f0f6fc;
125 padding: 16px 16px;
126 margin: 20px 0;
127 display: flex;
128 justify-content: space-between;
129 border-radius: 5px;
130 align-items: center;
131 border: 1px solid #ccc;
132 }
133 .tpt-add-role-card {
134 position: sticky;
135 top: 40px;
136 }
137 `}
138 </style>
139
140 <Flex align="center" justify="space-between" style={{marginBottom: '20px'}}>
141 <FlexItem>
142 <h2 style={{margin: 0}}>{__('Role-Based Tax Options', 'tier-pricing-table')}</h2>
143 <p style={{margin: '5px 0 0 0', color: '#646970'}}>
144 {__('Override default tax options for specific user roles.', 'tier-pricing-table')}
145 </p>
146 </FlexItem>
147 </Flex>
148
149 <input type="hidden" name="tpt_role_tax_settings" value={JSON.stringify(settings || {})}/>
150
151
152 {activeRoles.length === 0 ? (
153 <div style={{
154 padding: '40px',
155 textAlign: 'center',
156 background: '#fff',
157 border: '1px solid #c3c4c7',
158 borderRadius: '8px',
159 marginBottom: '24px',
160 color: '#50575e'
161 }}>
162 <div style={{display: 'flex', justifyContent: 'center', marginBottom: '12px', color: '#a7aaad'}}>
163 <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor"
164 strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
165 <path d="M18 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
166 <circle cx="10" cy="7" r="4"/>
167 <circle cx="18" cy="17" r="5" fill="#fff"/>
168 <line x1="16" y1="19" x2="20" y2="15"/>
169 <circle cx="16.5" cy="15.5" r="0.5" fill="currentColor"/>
170 <circle cx="19.5" cy="18.5" r="0.5" fill="currentColor"/>
171 </svg>
172 </div>
173 <h3 style={{margin: '0 0 8px', fontSize: '16px', color: '#1d2327', fontWeight: 600}}>
174 {__('No Custom Tax Configurations', 'tier-pricing-table')}
175 </h3>
176 <p style={{margin: '0 0 8px', fontSize: '14px'}}>
177 {__('Configure custom tax rules for specific user roles.', 'tier-pricing-table')}
178 </p>
179 <p style={{
180 margin: '0 0 20px',
181 fontSize: '13px',
182 color: '#646970',
183 maxWidth: '500px',
184 marginLeft: 'auto',
185 marginRight: 'auto'
186 }}>
187 {__('Select a role below to create your first configuration.', 'tier-pricing-table')}
188 </p>
189
190 <div style={{display: 'flex', gap: '10px', alignItems: 'center', justifyContent: 'center'}}>
191 <SelectControl
192 options={[
193 {label: __('Select a role...', 'tier-pricing-table'), value: ''},
194 ...availableRolesToAdd
195 ]}
196 value={newRole}
197 onChange={(val) => setNewRole(val)}
198 style={{marginBottom: 0}}
199 />
200 <Button variant="primary" onClick={addRole} disabled={!newRole}>
201 {__('Create Configuration', 'tier-pricing-table')}
202 </Button>
203 </div>
204 </div>
205 ) : (
206 <div className="tpt-accordion-container" style={{maxWidth: '650px'}}>
207 {activeRoles.map(role => {
208 const roleName = ROLES.find(r => r.value === role)?.label || role;
209 return (
210 <PanelBody
211 key={role}
212 className="tpt-role-settings-panel"
213 title={
214 <div style={{display: 'flex', alignItems: 'center', gap: '8px'}}>
215 <span
216 style={{fontWeight: 600}}>{__('Custom Tax Configuration', 'tier-pricing-table')}</span>
217 <span style={{
218 background: 'var(--wp-admin-theme-color, #2271b1)',
219 padding: '4px 10px',
220 borderRadius: '4px',
221 fontSize: '12px',
222 fontWeight: '600',
223 color: '#fff'
224 }}>
225 {roleName}
226 </span>
227 </div>
228 }
229 initialOpen={newlyAddedRoles.includes(role)}
230 >
231 {!window.tptTaxSettings?.isPremium && (
232 <div className="tpt-premium-banner">
233 <p style={{margin: '0', fontSize: '13px'}}>
234 <strong>{__('Premium Feature', 'tier-pricing-table')}</strong>: {__('Upgrading to premium to unlock role-based tax options.', 'tier-pricing-table')}
235 </p>
236 <a href={window.tptTaxSettings?.upgradeUrl || '#'} target="_blank"
237 rel="noopener noreferrer" className="button button-primary">
238 {__('Upgrade to Premium', 'tier-pricing-table')}
239 </a>
240 </div>
241 )}
242
243 <PanelRow>
244 <div style={{
245 width: '100%',
246 paddingTop: '20px',
247 maxWidth: '600px',
248 display: 'flex',
249 flexDirection: 'column',
250 gap: '20px'
251 }}>
252 <ToggleControl
253 label={__('Tax Exempt', 'tier-pricing-table')}
254 help={__('Disable tax calculation entirely for this role.', 'tier-pricing-table')}
255 checked={!!settings[role]?.tax_exempt}
256 onChange={(val) => updateRoleSetting(role, 'tax_exempt', val)}
257 />
258
259 {!settings[role]?.tax_exempt && (
260 <>
261 <RadioControl
262 label={__('Tax Class', 'tier-pricing-table')}
263 selected={settings[role]?.tax_class || 'default'}
264 options={[
265 {
266 label: __('Default (Inherit)', 'tier-pricing-table'),
267 value: 'default'
268 },
269 {
270 label: __('Standard', 'tier-pricing-table'),
271 value: 'standard'
272 },
273 {
274 label: __('Reduced Rate', 'tier-pricing-table'),
275 value: 'reduced-rate'
276 },
277 {
278 label: __('Zero Rate', 'tier-pricing-table'),
279 value: 'zero-rate'
280 }
281 ]}
282 onChange={(val) => updateRoleSetting(role, 'tax_class', val)}
283 />
284
285 <RadioControl
286 label={__('Prices Entered With Tax', 'tier-pricing-table')}
287 selected={settings[role]?.prices_include_tax || 'default'}
288 options={[
289 {
290 label: __('Default (Inherit)', 'tier-pricing-table'),
291 value: 'default'
292 },
293 {
294 label: __('Yes, I will enter prices inclusive of tax', 'tier-pricing-table'),
295 value: 'yes'
296 },
297 {
298 label: __('No, I will enter prices exclusive of tax', 'tier-pricing-table'),
299 value: 'no'
300 }
301 ]}
302 onChange={(val) => updateRoleSetting(role, 'prices_include_tax', val)}
303 />
304
305 <RadioControl
306 label={__('Display Prices in Shop', 'tier-pricing-table')}
307 selected={settings[role]?.display_shop || 'default'}
308 options={[
309 {
310 label: __('Default (Inherit)', 'tier-pricing-table'),
311 value: 'default'
312 },
313 {
314 label: __('Including Tax', 'tier-pricing-table'),
315 value: 'incl'
316 },
317 {
318 label: __('Excluding Tax', 'tier-pricing-table'),
319 value: 'excl'
320 }
321 ]}
322 onChange={(val) => updateRoleSetting(role, 'display_shop', val)}
323 />
324
325 <RadioControl
326 label={__('Display Prices in Cart/Checkout', 'tier-pricing-table')}
327 selected={settings[role]?.display_cart || 'default'}
328 options={[
329 {
330 label: __('Default (Inherit)', 'tier-pricing-table'),
331 value: 'default'
332 },
333 {
334 label: __('Including Tax', 'tier-pricing-table'),
335 value: 'incl'
336 },
337 {
338 label: __('Excluding Tax', 'tier-pricing-table'),
339 value: 'excl'
340 }
341 ]}
342 onChange={(val) => updateRoleSetting(role, 'display_cart', val)}
343 />
344
345 <TextControl
346 label={__('Price Display Suffix', 'tier-pricing-table')}
347 help={__('Leave blank to inherit default settings.', 'tier-pricing-table')}
348 value={settings[role]?.price_suffix !== undefined ? settings[role]?.price_suffix : ''}
349 onChange={(val) => updateRoleSetting(role, 'price_suffix', val)}
350 placeholder={__('N/A', 'tier-pricing-table')}
351 />
352 </>
353 )}
354
355 <div style={{
356 marginTop: '10px',
357 paddingTop: '20px',
358 borderTop: '1px solid #eee'
359 }}>
360 <Button
361 isDestructive
362 isTertiary
363 icon={trash}
364 onClick={() => removeRole(role)}
365 style={{padding: 0}}
366 >
367 {__('Remove Configuration', 'tier-pricing-table')}
368 </Button>
369 </div>
370 </div>
371 </PanelRow>
372 </PanelBody>
373 );
374 })}
375 </div>
376 )}
377
378 <Flex gap="4" style={{marginTop: '20px', alignItems: 'center'}}>
379 {activeRoles.length > 0 && availableRolesToAdd.length > 0 && (
380 <div style={{display: 'flex', gap: '10px', alignItems: 'center'}}>
381 <SelectControl
382 options={[
383 {label: __('Select a role...', 'tier-pricing-table'), value: ''},
384 ...availableRolesToAdd
385 ]}
386 value={newRole}
387 onChange={(val) => setNewRole(val)}
388 style={{marginBottom: 0, minWidth: '200px'}}
389 />
390 <Button variant="secondary" icon={plus} onClick={addRole} disabled={!newRole}>
391 {__('Add Configuration', 'tier-pricing-table')}
392 </Button>
393 </div>
394 )}
395 </Flex>
396 </div>
397 );
398 }
399
400 function sprintf(format, ...args) {
401 let i = 0;
402 return format.replace(/%s/g, () => args[i++]);
403 }
404