diff --git a/.wp-env.json b/.wp-env.json index b16393f..79ba4a3 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,12 +1,11 @@ { - "core": "WordPress/WordPress", "themes": [ - "https://downloads.wordpress.org/theme/storefront.3.8.1.zip" + "https://downloads.wordpress.org/theme/storefront.3.9.1.zip" ], "plugins": [ ".", - "https://downloads.wordpress.org/plugin/woocommerce.5.6.0.zip", - "https://downloads.wordpress.org/plugin/woocommerce-gateway-stripe.5.4.0.zip", - "https://downloads.wordpress.org/plugin/email-log.2.4.5.zip" + "https://downloads.wordpress.org/plugin/woocommerce.6.1.0.zip", + "https://downloads.wordpress.org/plugin/woocommerce-gateway-stripe.6.0.0.zip", + "https://downloads.wordpress.org/plugin/email-log.2.4.8.zip" ] } diff --git a/assets/admin.ts b/assets/admin.ts index d6c9940..320173e 100644 --- a/assets/admin.ts +++ b/assets/admin.ts @@ -3,7 +3,9 @@ import { render, createElement } from '@wordpress/element' import './admin.scss' import AdminApp from './admin/AdminApp' -const element = document.getElementById('tr-wc-settings') -if (element) { - render(createElement(AdminApp), element) +// eslint-disable-next-line import/prefer-default-export +export const appElement = document.getElementById('tr-wc-settings') + +if (appElement) { + render(createElement(AdminApp), appElement) } diff --git a/assets/admin/components/AdvancedCheckout/AdvancedCheckout.scss b/assets/admin/components/AdvancedCheckout/AdvancedCheckout.scss new file mode 100644 index 0000000..b3267b1 --- /dev/null +++ b/assets/admin/components/AdvancedCheckout/AdvancedCheckout.scss @@ -0,0 +1,10 @@ +.tr-wc-ruleBtn-group { + display: flex; + border-top: #e2e4e7 1px solid; + padding-top: 16px; + margin-top: 8px; + + .tr-wc-ruleBtn { + margin-right: 16px; + } +} diff --git a/assets/admin/components/AdvancedCheckout/AuditLogModal/AuditLogModal.scss b/assets/admin/components/AdvancedCheckout/AuditLogModal/AuditLogModal.scss new file mode 100644 index 0000000..ef1d3e2 --- /dev/null +++ b/assets/admin/components/AdvancedCheckout/AuditLogModal/AuditLogModal.scss @@ -0,0 +1,20 @@ +.tr-wc-auditLogModal-container { + width: 50vw; + max-width: 800px; + min-width: 700px; +} + +.tr-wc-auditLogModal-contentPanel { + height: 400px; +} + +.tr-wc-auditLogModal-loading { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + +.tr-wc-auditLogModal-panel { + padding-top: 16px; +} diff --git a/assets/admin/components/AdvancedCheckout/AuditLogModal/LogEntry/LogEntry.scss b/assets/admin/components/AdvancedCheckout/AuditLogModal/LogEntry/LogEntry.scss new file mode 100644 index 0000000..3c0f4e3 --- /dev/null +++ b/assets/admin/components/AdvancedCheckout/AuditLogModal/LogEntry/LogEntry.scss @@ -0,0 +1,19 @@ +.tr-wc-logEntry-table { + padding: 8px; + width: 100%; + border-collapse: collapse; + margin-top: 16px; + + &:first-child { + margin-top: 0; + } + + td { + border: 1px solid #ccc; + } +} + +.tr-wc-auditLogModal-dataCell { + box-sizing: border-box; + padding: 0 4px; +} diff --git a/assets/admin/components/AdvancedCheckout/AuditLogModal/LogEntry/index.tsx b/assets/admin/components/AdvancedCheckout/AuditLogModal/LogEntry/index.tsx new file mode 100644 index 0000000..3feadfd --- /dev/null +++ b/assets/admin/components/AdvancedCheckout/AuditLogModal/LogEntry/index.tsx @@ -0,0 +1,59 @@ +import React from 'react' + +import { AuditLogEntry } from '../../../../providers/Config/helpers' + +import './LogEntry.scss' + +const opLabels = { + EQ: 'gelijk aan', + NQ: 'niet gelijk aan', + GT: 'groter dan', + LT: 'kleiner dan', + CO: 'bevat', + SW: 'begint met', + EW: 'eindigt met', +} + +const LogEntry: React.FC = ({ orderId, timestamp, entries }) => ( + + + {entries.map((entry, index) => + entry.comparisons.map((comparison) => ( + + {index === 0 && ( + + )} + + + + + )), + )} + +
+ {`Bestelling #${orderId}`} +

{`${timestamp} UTC`}

+
+ {`${entry.fieldName} ${ + opLabels[comparison.operator as keyof typeof opLabels] + } ${comparison.compareValue}`} + +

Waarde: {entry.fieldValue}

+

+ Uitkomst:{' '} + + {String(comparison.result)} + +

+
+) + +export default LogEntry diff --git a/assets/admin/components/AdvancedCheckout/AuditLogModal/index.tsx b/assets/admin/components/AdvancedCheckout/AuditLogModal/index.tsx new file mode 100644 index 0000000..3cc88e6 --- /dev/null +++ b/assets/admin/components/AdvancedCheckout/AuditLogModal/index.tsx @@ -0,0 +1,53 @@ +import React from 'react' + +import { AuditLogEntry, findAuditLogs } from '../../../providers/Config/helpers' + +import CircularProgress from '../../CircularProgress' +import Modal from '../../Modal' + +import LogEntry from './LogEntry' + +import './AuditLogModal.scss' + +interface AuditLogModalProps { + open: boolean + onClose: () => void +} + +const AuditLogModal: React.FC = ({ open, onClose }) => { + const [isLoading, setLoading] = React.useState(true) + const [logs, setLogs] = React.useState([]) + + React.useEffect(() => { + if (!open) return + + setLoading(true) + + findAuditLogs() + .then(setLogs) + .finally(() => setLoading(false)) + }, [open]) + + return ( + + {!isLoading ? ( + logs.map((entry) => ) + ) : ( +
+ +
+ )} +
+ ) +} + +export default AuditLogModal diff --git a/assets/admin/components/AdvancedCheckout/RulesModal/EmptyView/EmptyView.scss b/assets/admin/components/AdvancedCheckout/RulesModal/EmptyView/EmptyView.scss new file mode 100644 index 0000000..e6b9ccf --- /dev/null +++ b/assets/admin/components/AdvancedCheckout/RulesModal/EmptyView/EmptyView.scss @@ -0,0 +1,20 @@ +.tr-wc-rulesModal-emptyView-panel { + position: absolute; + bottom: 8px; + left: 88px; +} + +.tr-wc-rulesModal-emptyView-line { + width: 230px; + color: #ccc; +} + +.tr-wc-rulesModal-emptyView-text { + display: flex; + align-items: center; + position: absolute; + top: -11px; + right: -180px; + width: 170px; + font-size: 16px; +} diff --git a/assets/admin/components/AdvancedCheckout/RulesModal/EmptyView/index.tsx b/assets/admin/components/AdvancedCheckout/RulesModal/EmptyView/index.tsx new file mode 100644 index 0000000..ef81d43 --- /dev/null +++ b/assets/admin/components/AdvancedCheckout/RulesModal/EmptyView/index.tsx @@ -0,0 +1,18 @@ +import React from 'react' + +import ConnectorLineDown from '../../../vectors/ConnectorLineDown' + +import './EmptyView.scss' +import AddFilter from '../../../vectors/AddFilter' + +const EmptyView: React.FC = () => ( +
+ +

+ + Voeg een regel toe +

+
+) + +export default EmptyView diff --git a/assets/admin/components/AdvancedCheckout/RulesModal/FieldInput/FieldInput.scss b/assets/admin/components/AdvancedCheckout/RulesModal/FieldInput/FieldInput.scss new file mode 100644 index 0000000..9f24349 --- /dev/null +++ b/assets/admin/components/AdvancedCheckout/RulesModal/FieldInput/FieldInput.scss @@ -0,0 +1,8 @@ +.tr-wc-fieldInput-container { + display: flex; + flex-direction: column; +} + +.tr-wc-fieldInput-input { + margin-top: 8px; +} diff --git a/assets/admin/components/AdvancedCheckout/RulesModal/FieldInput/index.tsx b/assets/admin/components/AdvancedCheckout/RulesModal/FieldInput/index.tsx new file mode 100644 index 0000000..782b0c0 --- /dev/null +++ b/assets/admin/components/AdvancedCheckout/RulesModal/FieldInput/index.tsx @@ -0,0 +1,93 @@ +import React from 'react' +import { useField } from 'formik' + +import './FieldInput.scss' + +interface FieldInputProps { + name: string +} + +const preSelectable = { + method_id: 'Verzendoptie code', + method_title: 'Verzendoptie naam', + payment_method: 'Betaalmethode type', + payment_method_title: 'Betaalmethode naam', + total: 'Order totaal', + status: 'Order status', + created_via: 'Aangemaakt via', +} +const selectableKeys = Object.keys(preSelectable) as Array< + keyof typeof preSelectable +> + +const FieldInput: React.FC = ({ name }) => { + const [field, , helpers] = useField(name) + const [isEditable, setEditable] = React.useState( + field.value && !selectableKeys.includes(field.value), + ) + + const handleSelectChanged = React.useCallback( + (event: React.ChangeEvent) => { + const { + target: { value }, + } = event + + const isNormalSelection = selectableKeys.includes( + value as keyof typeof preSelectable, + ) + if (isNormalSelection) { + helpers.setValue(value) + if (isEditable) { + setEditable(false) + } + } else { + helpers.setValue('') + setEditable(true) + } + }, + [helpers, isEditable], + ) + + return ( +
+ + + +
+ ) +} + +export default FieldInput diff --git a/assets/admin/components/AdvancedCheckout/RulesModal/Rule/Rule.scss b/assets/admin/components/AdvancedCheckout/RulesModal/Rule/Rule.scss new file mode 100644 index 0000000..9663d85 --- /dev/null +++ b/assets/admin/components/AdvancedCheckout/RulesModal/Rule/Rule.scss @@ -0,0 +1,57 @@ +.tr-wc-Rule-container { + display: flex; + align-items: center; + margin-top: 8px; + box-sizing: border-box; + + &:not(:last-child) { + padding-bottom: 8px; + border-bottom: 1px solid #e2e4e7; + } +} + +.tr-wc-Rule-ruleContainer { + display: flex; + flex: 1 auto; + justify-content: space-evenly; + align-items: center; + margin-right: 16px; + + & > * { + flex: 1; + + &:not(:last-child) { + margin-right: 8px; + } + } +} + +.tr-wc-Rule-select, +.tr-wc-Rule-input { + border-radius: 5px; + border: 1px solid #ccc; + color: #220C4A; + height: 32px; + + &:disabled { + color: #B8B8B8; + background-color: #F5F5F5; + outline: none; + } + + &:hover:not(:disabled) { + border-color: transparent !important; + outline: 2px #AE97FF solid; + } + + &:active:not(:disabled), &:focus:not(:disabled) { + border-color: transparent !important; + outline: 2px #8000FF solid; + } +} + +.tr-wc-Rule-input { + padding: 0 8px; +} + + diff --git a/assets/admin/components/AdvancedCheckout/RulesModal/Rule/index.tsx b/assets/admin/components/AdvancedCheckout/RulesModal/Rule/index.tsx new file mode 100644 index 0000000..f2d674f --- /dev/null +++ b/assets/admin/components/AdvancedCheckout/RulesModal/Rule/index.tsx @@ -0,0 +1,44 @@ +import React from 'react' +import { Field, FieldProps } from 'formik' + +import IconButton from '../../../IconButton' +import RemoveFilter from '../../../vectors/RemoveFilter' + +import './Rule.scss' +import FieldInput from '../FieldInput' + +interface RuleProps { + prefix: string + onRemove: () => void +} + +const Rule: React.FC = ({ prefix, onRemove }) => { + return ( +
+
+ + ) => ( + + )} + /> + +
+ +
+ } onClick={onRemove} /> +
+
+ ) +} + +export default Rule diff --git a/assets/admin/components/AdvancedCheckout/RulesModal/RulesModal.scss b/assets/admin/components/AdvancedCheckout/RulesModal/RulesModal.scss new file mode 100644 index 0000000..c2814f6 --- /dev/null +++ b/assets/admin/components/AdvancedCheckout/RulesModal/RulesModal.scss @@ -0,0 +1,19 @@ +.tr-wc-rulesModal-container { + width: 60vw; + max-width: 1000px; + min-width: 800px; +} + +.tr-wc-rulesModal-contentPanel { + height: 500px; +} + +.tr-wc-rulesModal-content { + position: relative; + margin-top: 16px; +} + +.tr-wc-rulesModal-footerContainer { + display: flex; + justify-content: space-between; +} diff --git a/assets/admin/components/AdvancedCheckout/RulesModal/index.tsx b/assets/admin/components/AdvancedCheckout/RulesModal/index.tsx new file mode 100644 index 0000000..f5f7e98 --- /dev/null +++ b/assets/admin/components/AdvancedCheckout/RulesModal/index.tsx @@ -0,0 +1,113 @@ +/* eslint-disable react/no-array-index-key */ +import React from 'react' +import { FormikProps, withFormik, FieldArray } from 'formik' + +import Save from '../../vectors/Save' +import AddFilter from '../../vectors/AddFilter' + +import Modal from '../../Modal' +import Button from '../../Button' + +import './RulesModal.scss' +import Rule from './Rule' +import EmptyView from './EmptyView' +import RulesSchema from './schema' +import CircularProgress from '../../CircularProgress' + +export interface RuleModel { + field: string + comparator: string + value: string +} + +interface RulesModalProps { + isLoading: boolean + open: boolean + rules?: RuleModel[] + onSaveRules: (rules: RuleModel[]) => void | Promise + onClose: () => void +} + +interface RulesModalValues { + rules: RuleModel[] +} + +const RulesModal: React.FC> = ({ + isLoading, + isValid, + submitForm, + values: { rules }, + setFieldValue, + open, + onClose, +}) => { + const handleAddRule = React.useCallback(() => { + setFieldValue('rules', [...rules, { comparator: 'EQ' }]) + }, [rules, setFieldValue]) + + return ( + + + + + + } + onClose={onClose} + > + + rules.map((rule, index) => ( + arrayHelpers.remove(index)} + /> + )) + } + /> + + {!rules.length ? : undefined} + + ) +} + +export default withFormik({ + displayName: 'RulesModalForm', + validationSchema: RulesSchema, + validateOnMount: true, + mapPropsToValues({ rules }): RulesModalValues { + return { + rules: rules ?? [], + } + }, + handleSubmit({ rules }, { props: { onSaveRules } }) { + onSaveRules(rules) + }, +})(RulesModal) diff --git a/assets/admin/components/AdvancedCheckout/RulesModal/schema.ts b/assets/admin/components/AdvancedCheckout/RulesModal/schema.ts new file mode 100644 index 0000000..bd7d7cf --- /dev/null +++ b/assets/admin/components/AdvancedCheckout/RulesModal/schema.ts @@ -0,0 +1,32 @@ +import * as Yup from 'yup' + +const separators = /[$%|]/ + +const RulesSchema = Yup.object().shape({ + rules: Yup.array() + .of( + Yup.object().shape({ + field: Yup.string() + .required() + .test( + 'Het veld mag geen $, | en % karakters bevatten.', + (value) => Boolean(value) && !separators.test(value as string), + ), + comparator: Yup.string() + .required() + .test( + 'Het veld mag geen $, | en % karakters bevatten.', + (value) => Boolean(value) && !separators.test(value as string), + ), + value: Yup.string() + .required() + .test( + 'Het veld mag geen $, | en % karakters bevatten.', + (value) => Boolean(value) && !separators.test(value as string), + ), + }), + ) + .min(1), +}) + +export default RulesSchema diff --git a/assets/admin/components/AdvancedCheckout/index.tsx b/assets/admin/components/AdvancedCheckout/index.tsx index 3c549d5..844b25b 100644 --- a/assets/admin/components/AdvancedCheckout/index.tsx +++ b/assets/admin/components/AdvancedCheckout/index.tsx @@ -1,35 +1,125 @@ import React from 'react' + +import Filter from '../vectors/Filter' +import Book from '../vectors/Book' + import Switch from '../Switch' +import Panel from '../Panel' +import Button from '../Button' + +import RulesModal, { RuleModel } from './RulesModal' + +import './AdvancedCheckout.scss' +import AuditLogModal from './AuditLogModal' interface AdvancedCheckoutProps { + isLoading: boolean + rules: RuleModel[] + isSubRenewalsEnabled: boolean allOrdersAreTrunkrs: boolean + isOrderFiltersEnabled: boolean onAllOrdersAreTrunkrs: () => void | Promise + onSaveRules: (rules: RuleModel[]) => void | Promise + onEnableOrderRules: () => void | Promise + onIsSubRenewalsEnabled: () => void | Promise } const AdvancedCheckout: React.FC = ({ + isLoading, + rules, + isSubRenewalsEnabled, allOrdersAreTrunkrs, + isOrderFiltersEnabled, onAllOrdersAreTrunkrs, + onEnableOrderRules, + onSaveRules, + onIsSubRenewalsEnabled, }) => { + const [isRulesOpen, setRulesOpen] = React.useState(false) + const [isLogsOpen, setLogsOpen] = React.useState(false) + + const handleToggleRules = React.useCallback(() => { + setRulesOpen((current) => !current) + }, []) + + const handleToggleLogs = React.useCallback(() => { + setLogsOpen((current) => !current) + }, []) + return ( -
- -

Verzendings instellingen

-
- + <> +
  • +

    + Maak een zending aan als abonnement verlenging is betaald. +

    +
    +
  • + +
  • +

    - Alle orders zijn voor Trunkrs + Alle orders zijn exclusief voor Trunkrs.

  • + +
  • + +

    + Gebruik order selectie filters. +

    +
    +
  • + +
  • + + + +
-
-
+ + + + + + ) } diff --git a/assets/admin/components/AppContainer/helpers.ts b/assets/admin/components/AppContainer/helpers.ts new file mode 100644 index 0000000..39782db --- /dev/null +++ b/assets/admin/components/AppContainer/helpers.ts @@ -0,0 +1,32 @@ +import { RuleModel } from '../AdvancedCheckout/RulesModal' + +const fieldSeparator = '%' +const valueSeparator = '$' +const ruleSeparator = '|' + +export const fromOrderRuleString = (orderRuleString?: string): RuleModel[] => { + if (!orderRuleString) return [] + + const fieldStrings = orderRuleString.split(fieldSeparator) + + return fieldStrings.map((fieldString) => { + const [fieldName, ruleString] = fieldString.split(valueSeparator) + const [operator, value] = ruleString.split(ruleSeparator) + + return { + field: fieldName, + comparator: operator, + value, + } + }) +} + +export const toOrderRuleString = (rules: RuleModel[]): string => { + return rules + .map((rule) => + [rule.field, [rule.comparator, rule.value].join(ruleSeparator)].join( + valueSeparator, + ), + ) + .join(fieldSeparator) +} diff --git a/assets/admin/components/AppContainer/index.tsx b/assets/admin/components/AppContainer/index.tsx index eeed179..dd1dd2c 100644 --- a/assets/admin/components/AppContainer/index.tsx +++ b/assets/admin/components/AppContainer/index.tsx @@ -8,10 +8,12 @@ import CenteredContainer from '../CenteredContainer' import ConnectionPanel from '../ConnectionPanel' import DetailsPanel from '../DetailsPanel' - -import './AppContainer.scss' import CheckoutPanel from '../CheckoutPanel' import AdvancedCheckout from '../AdvancedCheckout' +import { RuleModel } from '../AdvancedCheckout/RulesModal' + +import { fromOrderRuleString, toOrderRuleString } from './helpers' +import './AppContainer.scss' const AppContainer: React.FC = () => { const { @@ -23,14 +25,36 @@ const AppContainer: React.FC = () => { updateTntLinks, updateTntActions, updateAllOrdersAreTrunkrs, + updateUseOrderRules, + updateOrderRules, + updateIsSubRenewalsEnabled, } = useConfig() + const [isWorkingRules, setWorkingRules] = React.useState(false) + const handleLoginDone = React.useCallback( async (result: LoginResult): Promise => prepareConfig(result.accessToken, result.organizationId), [prepareConfig], ) + const handleSaveRules = React.useCallback( + async (rules: RuleModel[]) => { + try { + setWorkingRules(true) + await updateOrderRules(toOrderRuleString(rules)) + } finally { + setWorkingRules(false) + } + }, + [updateOrderRules], + ) + + const parsedRules = React.useMemo( + () => fromOrderRuleString(config?.orderRules), + [config?.orderRules], + ) + return ( @@ -46,8 +70,15 @@ const AppContainer: React.FC = () => { /> = ({ + type = 'button', color, + size, href, disabled, className, @@ -25,13 +29,14 @@ const Button: React.FC = ({ 'tr-wc-blue': color === 'blue', 'tr-wc-green': color === 'green', 'tr-wc-white': color === 'white', + 'tr-wc-button-small': size === 'small', }, className, ) const element = !href ? ( - // eslint-disable-next-line jsx-a11y/control-has-associated-label - + } + > + + -

Integratie nummer:

-

{integrationId}

-

Organisatie nummer:

-

{organizationId}

-

Organisatie naam:

-

{organizationName}

+

Deze winkel is verbonden

+

U bent klaar om uw bestellingen met Trunkrs te verzenden.

- - - - + +

Integratie nummer:

+

{integrationId}

+

Organisatie nummer:

+

{organizationId}

+

Organisatie naam:

+

{organizationName}

- + ) } diff --git a/assets/admin/components/IconButton/IconButton.scss b/assets/admin/components/IconButton/IconButton.scss new file mode 100644 index 0000000..648d75d --- /dev/null +++ b/assets/admin/components/IconButton/IconButton.scss @@ -0,0 +1,18 @@ +.tr-wc-iconButton-root { + display: flex; + justify-content: center; + align-items: center; + border: none; + appearance: none; + cursor: pointer; + background: none; + color: #220C4A; + box-sizing: border-box; + text-decoration: none !important; + outline: none !important; + padding: 0; +} + +.tr-wc-iconButton-icon { + width: 20px; +} diff --git a/assets/admin/components/IconButton/index.tsx b/assets/admin/components/IconButton/index.tsx new file mode 100644 index 0000000..f0f2d03 --- /dev/null +++ b/assets/admin/components/IconButton/index.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import clsx from 'clsx' + +import './IconButton.scss' + +interface IconButtonProps { + icon: React.ReactElement + className?: string + onClick?: () => void | Promise +} + +const IconButton: React.FC = ({ + icon, + className, + onClick, +}) => ( + +) + +export default IconButton diff --git a/assets/admin/components/Modal/Backdrop/Backdrop.scss b/assets/admin/components/Modal/Backdrop/Backdrop.scss new file mode 100644 index 0000000..f9ef02a --- /dev/null +++ b/assets/admin/components/Modal/Backdrop/Backdrop.scss @@ -0,0 +1,20 @@ +@keyframes backdrop-trans { + from { opacity: 0; } + to { opacity: 0.5; } +} + +.tr-wc-backdrop-root { + position: fixed; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 0; + bottom: 0; + right: 0; + opacity: 0.5; + z-index: 9999998; + background: #000; + animation-name: backdrop-trans; + animation-duration: 250ms; +} diff --git a/assets/admin/components/Modal/Backdrop/index.tsx b/assets/admin/components/Modal/Backdrop/index.tsx new file mode 100644 index 0000000..fbf6c1e --- /dev/null +++ b/assets/admin/components/Modal/Backdrop/index.tsx @@ -0,0 +1,30 @@ +/* eslint-disable jsx-a11y/no-noninteractive-element-interactions,jsx-a11y/click-events-have-key-events */ +import React from 'react' +import clsx from 'clsx' + +import './Backdrop.scss' + +interface BackdropProps { + open: boolean + className?: string + onClick?: () => void | Promise +} + +const Backdrop: React.FC = ({ + open, + className, + children, + onClick, +}) => ( +
+ {children} +
+) + +export default Backdrop diff --git a/assets/admin/components/Modal/Modal.scss b/assets/admin/components/Modal/Modal.scss new file mode 100644 index 0000000..480c4b8 --- /dev/null +++ b/assets/admin/components/Modal/Modal.scss @@ -0,0 +1,50 @@ +.tr-wc-modal-root { + position: fixed; + z-index: 9999999; +} + +.tr-wc-modal-rootContainer { + display: flex; + flex-direction: column; + width: 100%; + min-width: 600px; + background-color: #fff; + border-radius: 5px; +} + +.tr-wc-modal-header, +.tr-wc-modal-content, +.tr-wc-modal-footer { + box-sizing: border-box; +} + +.tr-wc-modal-contentContainer, +.tr-wc-modal-footer { + width: 100%; +} + +.tr-wc-modal-contentContainer { + display: flex; + flex-direction: column; +} + +.tr-wc-modal-header { + display: flex; + justify-content: space-between; + padding: 16px 16px 0; + + & > h3 { + margin: 0; + } +} + +.tr-wc-modal-content { + flex: 1; + padding: 0 16px 16px; + overflow-x: hidden; + overflow-y: auto; +} + +.tr-wc-modal-footer { + padding: 16px; +} diff --git a/assets/admin/components/Modal/helpers.ts b/assets/admin/components/Modal/helpers.ts new file mode 100644 index 0000000..245d24a --- /dev/null +++ b/assets/admin/components/Modal/helpers.ts @@ -0,0 +1,19 @@ +// eslint-disable-next-line import/prefer-default-export +export const getContentHeightOffset = (): number => { + const adminBar = document.getElementById('wpadminbar') + const content = document.getElementById('wpbody-content') + + let contentOffset = 0 + if (content) { + const padTop = window + .getComputedStyle(content, null) + .getPropertyValue('padding-top') + const padBot = window + .getComputedStyle(content, null) + .getPropertyValue('padding-bottom') + + contentOffset = parseInt(padTop, 10) + parseInt(padBot, 10) + } + + return (adminBar?.clientHeight ?? 32) + contentOffset +} diff --git a/assets/admin/components/Modal/index.tsx b/assets/admin/components/Modal/index.tsx new file mode 100644 index 0000000..93196af --- /dev/null +++ b/assets/admin/components/Modal/index.tsx @@ -0,0 +1,106 @@ +import React from 'react' +import clsx from 'clsx' +import EventListener from 'react-event-listener' + +import Portal from '../Portal' +import IconButton from '../IconButton' +import Close from '../vectors/Close' + +import Backdrop from './Backdrop' +import './Modal.scss' + +interface ModalProps { + open: boolean + classes?: { + root?: string + backdrop?: string + container?: string + header?: string + contentPanel?: string + content?: string + footer?: string + } + title?: string + footerContent?: React.ReactElement + onClose?: () => void +} + +const Modal: React.FC = ({ + open, + classes, + title, + footerContent, + children, + onClose, +}) => { + const rootRef = React.useRef(null) + const containerRef = React.useRef(null) + + const handleReCalculation = React.useCallback(() => { + const { current: rootEl } = rootRef + const { current: containerEl } = containerRef + + if (!containerEl || !rootEl) return + + const { clientHeight, clientWidth } = containerEl + + rootEl.setAttribute( + 'style', + `top: calc(50vh - ${Math.round( + clientHeight / 2, + )}px);left: calc(50vw - ${Math.round( + clientWidth / 2, + )}px); width: ${clientWidth}px;height: ${clientHeight}px;`, + ) + }, []) + + React.useEffect(handleReCalculation, [handleReCalculation, open]) + + return open ? ( + <> + + + + + +
+
+
+ + {title ?

{title}

: } + } onClick={onClose} /> + + + + {children} + +
+ + {footerContent && ( + + {footerContent} + + )} +
+
+
+ + ) : null +} + +export default Modal diff --git a/assets/admin/components/Panel/Panel.scss b/assets/admin/components/Panel/Panel.scss new file mode 100644 index 0000000..7b8cdf7 --- /dev/null +++ b/assets/admin/components/Panel/Panel.scss @@ -0,0 +1,47 @@ +.tr-wc-panelContainer { + display: flex; + flex-direction: column; + width: 700px; + border: 1px solid rgb(226, 228, 231); + border-radius: 3px; + box-shadow: 0 1px 1px rgb(0 0 0 / 4%); + background-color: #fff; + margin-bottom: 32px; +} + +.tr-wc-panelHeader { + display: flex; + align-items: center; + flex: 1; + padding: 16px 24px; + border-bottom: 1px solid rgb(226, 228, 231); + + & > p { + margin: 0; + font-size: 14px; + } +} + +.tr-wc-panelContent, +.tr-wc-panelFooter { + padding: 16px 24px; +} + +.tr-wc-panelContent { + display: flex; + flex-direction: column; + justify-content: space-between; + flex: 1; +} + +.tr-wc-panelFooter { + display: flex; + justify-content: flex-end; + border-top: 1px solid rgb(226, 228, 231); + + .tr-wc-buttonVector { + width: 16px; + margin-right: 8px; + margin-top: 1px; + } +} diff --git a/assets/admin/components/Panel/index.tsx b/assets/admin/components/Panel/index.tsx new file mode 100644 index 0000000..6ce1313 --- /dev/null +++ b/assets/admin/components/Panel/index.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import clsx from 'clsx' + +import './Panel.scss' + +interface PanelProps { + title: string + footerContent?: React.ReactElement + classes?: { + root?: string + header?: string + content?: string + footer?: string + } +} + +const Panel: React.FC = ({ + title, + footerContent, + classes, + children, +}) => { + return ( +
+ +

{title}

+
+ + + {children} + + + {footerContent && ( + + {footerContent} + + )} +
+ ) +} + +export default Panel diff --git a/assets/admin/components/Portal/index.tsx b/assets/admin/components/Portal/index.tsx new file mode 100644 index 0000000..562d664 --- /dev/null +++ b/assets/admin/components/Portal/index.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import ReactDOM from 'react-dom' + +type PortalRefFunction = (node: HTMLDivElement | null) => void + +interface PortalProps { + portalRef?: PortalRefFunction | React.MutableRefObject +} + +const portalRoot = document.getElementById('portal-root') + +class Portal extends React.PureComponent { + private readonly portalEl: HTMLDivElement + + public constructor(props: PortalProps) { + super(props) + + this.portalEl = document.createElement('div') + } + + public componentDidMount(): void { + portalRoot?.appendChild(this.portalEl) + this.updateRef(this.portalEl) + } + + public componentWillUnmount(): void { + portalRoot?.removeChild(this.portalEl) + this.updateRef(null) + } + + private updateRef = (node: HTMLDivElement | null): void => { + const { portalRef } = this.props + + if (!portalRef) { + return + } + + if (typeof portalRef === 'function') { + portalRef(node) + } else { + portalRef.current = node + } + } + + public render() { + const { children } = this.props + + return ReactDOM.createPortal(children, this.portalEl) + } +} + +export default Portal diff --git a/assets/admin/components/Switch/Switch.scss b/assets/admin/components/Switch/Switch.scss index 478cc3b..e9d7ed6 100644 --- a/assets/admin/components/Switch/Switch.scss +++ b/assets/admin/components/Switch/Switch.scss @@ -19,6 +19,17 @@ } } + &.tr-wc-switch-disabled { + cursor: default; + pointer-events: none; + background-color: #CCC; + + .tr-wc-switchKnob { + border-color: #CCC; + background-color: #F5F5F5; + } + } + .tr-wc-switchKnob { position: absolute; display: block; diff --git a/assets/admin/components/Switch/index.tsx b/assets/admin/components/Switch/index.tsx index 73e5a81..7391411 100644 --- a/assets/admin/components/Switch/index.tsx +++ b/assets/admin/components/Switch/index.tsx @@ -4,12 +4,14 @@ import clsx from 'clsx' import './Switch.scss' interface SwitchProps { + disabled?: boolean checked?: boolean tabIndex?: number onChange?: () => void | Promise } const Switch: React.FC = ({ + disabled, checked, tabIndex = 1, children, @@ -20,8 +22,11 @@ const Switch: React.FC = ({
diff --git a/assets/admin/components/vectors/AddFilter.tsx b/assets/admin/components/vectors/AddFilter.tsx new file mode 100644 index 0000000..1e360ae --- /dev/null +++ b/assets/admin/components/vectors/AddFilter.tsx @@ -0,0 +1,21 @@ +import React from 'react' + +interface CheckProps { + className?: string +} + +const AddFilter: React.FC = ({ className }) => ( + + + +) + +export default AddFilter diff --git a/assets/admin/components/vectors/Book.tsx b/assets/admin/components/vectors/Book.tsx new file mode 100644 index 0000000..2c918ff --- /dev/null +++ b/assets/admin/components/vectors/Book.tsx @@ -0,0 +1,21 @@ +import React from 'react' + +interface CheckProps { + className?: string +} + +const Book: React.FC = ({ className }) => ( + + + +) + +export default Book diff --git a/assets/admin/components/vectors/Check.tsx b/assets/admin/components/vectors/Check.tsx index db180a3..a683c74 100644 --- a/assets/admin/components/vectors/Check.tsx +++ b/assets/admin/components/vectors/Check.tsx @@ -9,7 +9,7 @@ const Check: React.FC = ({ className }) => ( xmlns="http://www.w3.org/2000/svg" className={className} viewBox="0 0 14 14" - fill="none" + fill="currentColor" > = ({ className }) => ( + + + +) + +export default Close diff --git a/assets/admin/components/vectors/ConnectorLineDown.tsx b/assets/admin/components/vectors/ConnectorLineDown.tsx new file mode 100644 index 0000000..7d4cfdc --- /dev/null +++ b/assets/admin/components/vectors/ConnectorLineDown.tsx @@ -0,0 +1,21 @@ +import React from 'react' + +interface CheckProps { + className?: string +} + +const Close: React.FC = ({ className }) => ( + + + +) + +export default Close diff --git a/assets/admin/components/vectors/Filter.tsx b/assets/admin/components/vectors/Filter.tsx new file mode 100644 index 0000000..7aade2a --- /dev/null +++ b/assets/admin/components/vectors/Filter.tsx @@ -0,0 +1,21 @@ +import React from 'react' + +interface CheckProps { + className?: string +} + +const Filter: React.FC = ({ className }) => ( + + + +) + +export default Filter diff --git a/assets/admin/components/vectors/Linkout.tsx b/assets/admin/components/vectors/Linkout.tsx index 661e2b8..6712785 100644 --- a/assets/admin/components/vectors/Linkout.tsx +++ b/assets/admin/components/vectors/Linkout.tsx @@ -9,7 +9,7 @@ const Linkout: React.FC = ({ className }) => ( xmlns="http://www.w3.org/2000/svg" className={className} viewBox="0 0 24 24" - fill="none" + fill="currentColor" > = ({ className }) => ( + + + +) + +export default RemoveFilter diff --git a/assets/admin/components/vectors/Save.tsx b/assets/admin/components/vectors/Save.tsx new file mode 100644 index 0000000..fa91637 --- /dev/null +++ b/assets/admin/components/vectors/Save.tsx @@ -0,0 +1,21 @@ +import React from 'react' + +interface CheckProps { + className?: string +} + +const Save: React.FC = ({ className }) => ( + + + +) + +export default Save diff --git a/assets/admin/hooks/useAppElement.ts b/assets/admin/hooks/useAppElement.ts new file mode 100644 index 0000000..bc03ff2 --- /dev/null +++ b/assets/admin/hooks/useAppElement.ts @@ -0,0 +1,12 @@ +const appElement = document.getElementById('tr-wc-settings') + +const features = { + element: appElement as HTMLDivElement, + setStyle: (cssString: string) => { + appElement?.setAttribute('style', cssString) + }, +} + +const useAppElement = () => features + +export default useAppElement diff --git a/assets/admin/providers/Config/Provider.tsx b/assets/admin/providers/Config/Provider.tsx index 5ce298b..6022eb1 100644 --- a/assets/admin/providers/Config/Provider.tsx +++ b/assets/admin/providers/Config/Provider.tsx @@ -10,6 +10,9 @@ import { doUpdateUseTntAccountsRequest, doUpdateUseAllOrdersAreTrunkrsRequest, doUpdateUseBigTextRequest, + doUpdateOrderRulesEnabled, + doUpdateOrderRules, + doUpdateSubRenewalsEnabled, } from './helpers' const initialConfigText = document.getElementById('__tr-wc-settings__') @@ -65,7 +68,7 @@ const ConfigProvider: React.FC = ({ children }) => { doUpdateUseDarkRequest(!config.isDarkLogo).catch(() => { setConfig({ ...config, - isDarkLogo: !config.isDarkLogo, + isDarkLogo: config.isDarkLogo, }) }) }, [config]) @@ -79,7 +82,7 @@ const ConfigProvider: React.FC = ({ children }) => { doUpdateUseTntLinksRequest(!config.isEmailLinksEnabled).catch(() => { setConfig({ ...config, - isEmailLinksEnabled: !config.isEmailLinksEnabled, + isEmailLinksEnabled: config.isEmailLinksEnabled, }) }) }, [config]) @@ -94,7 +97,7 @@ const ConfigProvider: React.FC = ({ children }) => { () => { setConfig({ ...config, - isAccountTrackTraceEnabled: !config.isAccountTrackTraceEnabled, + isAccountTrackTraceEnabled: config.isAccountTrackTraceEnabled, }) }, ) @@ -111,7 +114,21 @@ const ConfigProvider: React.FC = ({ children }) => { ).catch(() => { setConfig({ ...config, - isAllOrdersAreTrunkrsEnabled: !config.isAllOrdersAreTrunkrsEnabled, + isAllOrdersAreTrunkrsEnabled: config.isAllOrdersAreTrunkrsEnabled, + }) + }) + }, [config]) + + const updateIsSubRenewalsEnabled = React.useCallback(async () => { + setConfig({ + ...config, + isSubRenewalsEnabled: !config.isSubRenewalsEnabled, + }) + + doUpdateSubRenewalsEnabled(!config.isSubRenewalsEnabled).catch(() => { + setConfig({ + ...config, + isSubRenewalsEnabled: config.isSubRenewalsEnabled, }) }) }, [config]) @@ -125,11 +142,36 @@ const ConfigProvider: React.FC = ({ children }) => { doUpdateUseBigTextRequest(!config.isBigTextEnabled).catch(() => { setConfig({ ...config, - isBigTextEnabled: !config.isBigTextEnabled, + isBigTextEnabled: config.isBigTextEnabled, + }) + }) + }, [config]) + + const updateUseOrderRules = React.useCallback(async () => { + setConfig({ + ...config, + isOrderRulesEnabled: !config.isOrderRulesEnabled, + }) + + doUpdateOrderRulesEnabled(!config.isOrderRulesEnabled).catch(() => { + setConfig({ + ...config, + isOrderRulesEnabled: config.isOrderRulesEnabled, }) }) }, [config]) + const updateOrderRules = React.useCallback( + async (orderRules: string) => { + setConfig({ ...config, orderRules }) + + doUpdateOrderRules(orderRules).catch(() => { + setConfig({ ...config, orderRules }) + }) + }, + [config], + ) + const contextValue = React.useMemo( () => ({ isWorking, @@ -140,6 +182,9 @@ const ConfigProvider: React.FC = ({ children }) => { updateTntActions, updateAllOrdersAreTrunkrs, updateUseBigText, + updateUseOrderRules, + updateOrderRules, + updateIsSubRenewalsEnabled, }), [ config, @@ -150,6 +195,9 @@ const ConfigProvider: React.FC = ({ children }) => { updateTntLinks, updateAllOrdersAreTrunkrs, updateUseBigText, + updateUseOrderRules, + updateOrderRules, + updateIsSubRenewalsEnabled, ], ) diff --git a/assets/admin/providers/Config/helpers.ts b/assets/admin/providers/Config/helpers.ts index 3ceb1c6..2c96d50 100644 --- a/assets/admin/providers/Config/helpers.ts +++ b/assets/admin/providers/Config/helpers.ts @@ -8,6 +8,24 @@ interface IntegrationResponse { accessToken: string } +export interface AuditLogEntry { + orderId: string + timestamp: string + entries: [ + { + fieldName: string + fieldValue: string + comparisons: [ + { + operator: string + compareValue: string + result: boolean + }, + ] + }, + ] +} + export const decodeHtmlString = (htmlEncoded: string): string => { const txt = document.createElement('textarea') txt.innerHTML = htmlEncoded @@ -140,3 +158,59 @@ export const doUpdateUseBigTextRequest = async ( data: request, }) } + +export const doUpdateSubRenewalsEnabled = async ( + isEnabled: boolean, +): Promise => { + const request = new FormData() + request.append('action', 'tr-wc_update-use-sub-renewals') + request.append('isSubRenewalsEnabled', isEnabled.toString()) + + await Axios.request({ + method: 'POST', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + url: ajaxurl, + data: request, + }) +} + +export const doUpdateOrderRulesEnabled = async ( + isEnabled: boolean, +): Promise => { + const request = new FormData() + request.append('action', 'tr-wc_update-use-order-rules') + request.append('isOrderRulesEnabled', isEnabled.toString()) + + await Axios.request({ + method: 'POST', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + url: ajaxurl, + data: request, + }) +} + +export const doUpdateOrderRules = async (orderRules: string): Promise => { + const request = new FormData() + request.append('action', 'tr-wc_update-order-rules') + request.append('orderRules', orderRules) + + await Axios.request({ + method: 'POST', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + url: ajaxurl, + data: request, + }) +} + +export const findAuditLogs = async (): Promise => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const { data } = await Axios.get(`${ajaxurl}`, { + params: { action: 'tr-wc_get-order-logs' }, + }) + + return data +} diff --git a/assets/admin/providers/Config/index.tsx b/assets/admin/providers/Config/index.tsx index c43502e..68c6016 100644 --- a/assets/admin/providers/Config/index.tsx +++ b/assets/admin/providers/Config/index.tsx @@ -7,6 +7,9 @@ export interface Configuration { isEmailLinksEnabled: boolean isAccountTrackTraceEnabled: boolean isAllOrdersAreTrunkrsEnabled: boolean + isOrderRulesEnabled: boolean + isSubRenewalsEnabled: boolean + orderRules: string details: { integrationId: string organizationId: string @@ -24,6 +27,9 @@ export type ConfigContext = { updateTntLinks: () => Promise updateTntActions: () => Promise updateAllOrdersAreTrunkrs: () => Promise + updateUseOrderRules: () => Promise + updateIsSubRenewalsEnabled: () => Promise + updateOrderRules: (orderRules: string) => Promise } const ConfigContext = React.createContext({ @@ -47,6 +53,15 @@ const ConfigContext = React.createContext({ updateAllOrdersAreTrunkrs: () => { throw new Error('Not implemented!') }, + updateUseOrderRules: () => { + throw new Error('Not implemented!') + }, + updateOrderRules: () => { + throw new Error('Not implemented!') + }, + updateIsSubRenewalsEnabled: () => { + throw new Error('Not implemented!') + }, }) export default ConfigContext diff --git a/composer.json b/composer.json deleted file mode 100644 index e1a7b98..0000000 --- a/composer.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "trunkrs/woocommerce", - "type": "wordpress-plugin", - "license": "MIT", - "authors": [ - { - "name": "Trunkrs", - "email": "tech.support@trunkrs.nl" - } - ], - "require": {} -} diff --git a/composer.lock b/composer.lock deleted file mode 100644 index e18c65e..0000000 --- a/composer.lock +++ /dev/null @@ -1,18 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "26c06412ef46be6d5c15b43c207b73e9", - "packages": [], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [], - "plugin-api-version": "2.0.0" -} diff --git a/includes/admin/admin-endpoints.php b/includes/admin/admin-endpoints.php index 7b3efb2..3e038ba 100644 --- a/includes/admin/admin-endpoints.php +++ b/includes/admin/admin-endpoints.php @@ -6,12 +6,18 @@ class TRUNKRS_WC_AdminEndpoints const DOWNLOAD_LABEL_ACTION = 'tr-wc_download-label'; const CANCEL_ACTION = 'tr-wc_cancel'; const RE_ANNOUNCE_ACTION = 'tr-wc_reannounce'; + const GET_ORDER_LOGS = 'tr-wc_get-order-logs'; + const REGISTER_ACTION = 'tr-wc_register-plugin'; + const UPDATE_USE_DARK_ACTION = 'tr-wc_update-use-dark'; const UPDATE_USE_TNT_LINKS_ACTION = 'tr-wc_update-use-tnt-links'; const UPDATE_USE_TNT_ACCOUNT_ACTION = 'tr-wc_update-use-tnt-account'; const UPDATE_USE_ALL_ORDERS_ARE_TRUNKRS = 'tr-wc_update-use-all-orders-are-trunkrs'; const UPDATE_USE_BIG_CHECKOUT_TEXT = 'tr-wc_update-use-big-checkout-text'; + const UPDATE_USE_ORDER_RULES = 'tr-wc_update-use-order-rules'; + const UPDATE_ORDER_RULES = 'tr-wc_update-order-rules'; + const UPDATE_USE_SUB_RENEWALS = 'tr-wc_update-use-sub-renewals'; public function __construct() { @@ -21,10 +27,14 @@ public function __construct() add_action('wp_ajax_' . self::UPDATE_USE_TNT_ACCOUNT_ACTION, [$this, 'executeUpdateUseTnTAccountEndpoint']); add_action('wp_ajax_' . self::UPDATE_USE_ALL_ORDERS_ARE_TRUNKRS, [$this, 'executeUpdateUseAllOrdersAreTrunkrsEndpoint']); add_action('wp_ajax_' . self::UPDATE_USE_BIG_CHECKOUT_TEXT, [$this, 'executeUpdateUseBigCheckoutText']); + add_action('wp_ajax_' . self::UPDATE_USE_ORDER_RULES, [$this, 'executeUpdateUseOrderRules']); + add_action('wp_ajax_' . self::UPDATE_ORDER_RULES, [$this, 'executeUpdateOrderRuleSet']); + add_action('wp_ajax_' . self::UPDATE_USE_SUB_RENEWALS, [$this, 'executeUpdateUseSubRenewals']); add_action('wp_ajax_' . self::DOWNLOAD_LABEL_ACTION, [$this, 'executeDownloadLabelEndpoint']); add_action('wp_ajax_' . self::CANCEL_ACTION, [$this, 'executeCancelEndpoint']); add_action('wp_ajax_' . self::RE_ANNOUNCE_ACTION, [$this, 'executeAnnounceEndpoint']); + add_action('wp_ajax_' . self::GET_ORDER_LOGS, [$this, 'executeFindAuditLogEntries']); } public function executeRegisterEndpoint() @@ -101,6 +111,40 @@ public function executeUpdateUseBigCheckoutText() { wp_die(); } + public function executeUpdateUseOrderRules() { + $value = sanitize_text_field($_POST['isOrderRulesEnabled']) === 'true'; + + TRUNKRS_WC_Settings::setIsRuleEngineEnabled($value); + + status_header(204); + wp_die(); + } + + public function executeUpdateUseSubRenewals() { + $value = sanitize_text_field($_POST['isSubRenewalsEnabled']) === 'true'; + + TRUNKRS_WC_Settings::setUseSubscriptionRenewals($value); + + status_header(204); + wp_die(); + } + + public function executeUpdateOrderRuleSet() { + $value = sanitize_text_field($_POST['orderRules']); + + TRUNKRS_WC_Settings::setRules($value); + + status_header(204); + wp_die(); + } + + public function executeFindAuditLogEntries() { + $entries = TRUNKRS_WC_AuditLog::findLatestAuditLogs(); + + wp_send_json($entries); + wp_die(); + } + public function executeDownloadLabelEndpoint() { $trunkrsNr = sanitize_text_field($_GET['trunkrsNr']); $labelUrl = TRUNKRS_WC_Api::getLabel($trunkrsNr); diff --git a/includes/admin/admin-page.php b/includes/admin/admin-page.php index c75cd82..2228e86 100644 --- a/includes/admin/admin-page.php +++ b/includes/admin/admin-page.php @@ -37,6 +37,9 @@ public function renderAdminPageHtml() 'isEmailLinksEnabled' => TRUNKRS_WC_Settings::getUseTrackTraceLinks(), 'isAccountTrackTraceEnabled' => TRUNKRS_WC_Settings::getUseAccountActions(), 'isAllOrdersAreTrunkrsEnabled' => TRUNKRS_WC_Settings::getUseAllOrdersAreTrunkrsActions(), + 'isOrderRulesEnabled' => TRUNKRS_WC_Settings::isRuleEngineEnabled(), + 'isSubRenewalsEnabled' => TRUNKRS_WC_Settings::getUseSubscriptionRenewals(), + 'orderRules' => TRUNKRS_WC_Settings::getOrderRuleSet(), 'details' => TRUNKRS_WC_Settings::getIntegrationDetails(), 'metaBag' => [ 'php_version' => phpversion(), @@ -49,7 +52,9 @@ public function renderAdminPageHtml() ]) ?> +
+
$order->get_total(), + 'country' => $order->get_shipping_country(), + ], true); + + $isAvailable = TRUNKRS_WC_Utils::findInArray($available, function ($rate) use ($deliveryDate) { + return TRUNKRS_WC_Utils::parse8601($rate->deliveryDate)->format('Y-m-d') === $deliveryDate; + }); + + if (!is_null($isAvailable)) { + $singleShipmentBody['intendedDeliveryDate'] = $deliveryDate; + } } if (!empty($companyName)) { $singleShipmentBody['recipient']['companyName'] = $companyName; @@ -104,7 +115,13 @@ public static function announceShipment($order, string $reference, $deliveryDate 6000 ); - if (is_null($response) || is_wp_error($response) || $response['response']['code'] > 201) { + if (is_null($response) || is_wp_error($response)) { + return null; + } + + if ($response['response']['code'] === 404) { + return TRUNKRS_WC_Api::announceShipment($order, $reference); + } else if ($response['response']['code'] > 201) { return null; } @@ -124,7 +141,7 @@ public static function announceShipment($order, string $reference, $deliveryDate * Retrieves the current shipping rates for the integration. * @return array The current shipping rates. */ - public static function getShippingRates(array $orderDetails): array + public static function getShippingRates(array $orderDetails, bool $includeAll = false): array { $response = self::makeRequest( 'GET', @@ -138,7 +155,7 @@ public static function getShippingRates(array $orderDetails): array $rates = json_decode($response['body']); // For now only take the first one - return empty($rates) ? $rates : [$rates[0]]; + return empty($rates) ? $rates : $includeAll ? $rates : [$rates[0]]; } /** diff --git a/includes/index.php b/includes/index.php new file mode 100644 index 0000000..9331cc4 --- /dev/null +++ b/includes/index.php @@ -0,0 +1,8 @@ +get_charset_collate(); + $table_name = $wpdb->prefix . self::LOG_TABLE_NAME; + + $sql = "CREATE TABLE IF NOT EXISTS $table_name ( + order_id int NOT NULL, + timestamp datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, + json_data text NOT NULL, + PRIMARY KEY (order_id, timestamp) + ) $charset_collate;"; + + dbDelta($sql); + } + + public static function init() + { + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + + self::initAuditLog(); + } + } +} diff --git a/includes/order-rules/audit-log-rule-entry.php b/includes/order-rules/audit-log-rule-entry.php new file mode 100644 index 0000000..dbd9590 --- /dev/null +++ b/includes/order-rules/audit-log-rule-entry.php @@ -0,0 +1,44 @@ +fieldName = $fieldName; + $this->fieldValue = $value; + } + + /** + * Sets the result of the rule matching. + * @param TRUNKRS_WC_OrderRule $rule The evaluated rule. + * @param bool $result The execution result. + * @return void + */ + public function setResult(TRUNKRS_WC_OrderRule $rule, bool $result) { + $this->results[] = [ + 'operator' => $rule->operator, + 'compareValue' => $rule->value, + 'result' => $result + ]; + } + } +} + diff --git a/includes/order-rules/audit-log.php b/includes/order-rules/audit-log.php new file mode 100644 index 0000000..877361c --- /dev/null +++ b/includes/order-rules/audit-log.php @@ -0,0 +1,104 @@ +prefix . self::LOG_TABLE_NAME; + + $results = $wpdb->get_results( + " + SELECT order_id, + timestamp, + json_data + FROM $tableName + ORDER BY timestamp DESC + LIMIT 10; + " + ); + + return array_map(function ($row) { + return [ + 'orderId' => $row->order_id, + 'timestamp' => $row->timestamp, + 'entries' => json_decode($row->json_data), + ]; + }, $results); + } + + /** + * The order id of the evaluated order. + * @var int + */ + var $orderId; + + /** + * The audit log entries + * @var TRUNKRS_WC_AuditLogRuleEntry[] + */ + var $entries; + + public function __construct(int $orderId) + { + $this->orderId = $orderId; + } + + /** + * Create a new audit log entry. + * @return TRUNKRS_WC_AuditLogRuleEntry + */ + public function createEntry(string $fieldName, $fieldValue) + { + return $this->entries[] = new TRUNKRS_WC_AuditLogRuleEntry($fieldName, $fieldValue); + } + + /** + * Save the log into the database. + * @return void + */ + public function saveLog() + { + global $wpdb; + + $wpdb->insert( + $wpdb->prefix . self::LOG_TABLE_NAME, + [ + 'order_id' => $this->orderId, + 'json_data' => json_encode($this->asArray()['entries']), + ], + ['%d', '%s'] + ); + } + + /** + * Yields an array representation of the audit log entry. + * @return array + */ + public function asArray(): array + { + return [ + 'orderId' => $this->orderId, + 'entries' => array_map(function ($entry) { + return [ + 'fieldName' => $entry->fieldName, + 'fieldValue' => $entry->fieldValue, + 'comparisons' => $entry->results, + ]; + }, $this->entries), + ]; + } + } +} + diff --git a/includes/order-rules/index.php b/includes/order-rules/index.php new file mode 100644 index 0000000..bf95397 --- /dev/null +++ b/includes/order-rules/index.php @@ -0,0 +1,7 @@ +operator = $values[0]; + $this->value = $values[1]; + } + + /** + * Checks whether the value matches the field. + * @param string $value The field value + * @return bool A value reflecting whether the field matches the rule condition. + */ + public function matches(string $value): bool + { + switch ($this->operator) { + case TRUNKRS_WC_RuleOperator::EQUALS: + return $value == $this->value; + + case TRUNKRS_WC_RuleOperator::NOT_EQUALS: + return $value != $this->value; + + case TRUNKRS_WC_RuleOperator::GREATER_THAN: + return $value > $this->value; + + case TRUNKRS_WC_RuleOperator::LOWER_THAN: + return $value < $this->value; + + case TRUNKRS_WC_RuleOperator::CONTAINS: + return strpos($value, $this->value) !== false; + + case TRUNKRS_WC_RuleOperator::STARTS_WITH: + return substr($value, 0, strlen($this->value)) === $this->value; + + case TRUNKRS_WC_RuleOperator::ENDS_WITH: + return substr($value, -strlen($this->value)) === $this->value; + + default: + return false; + } + } + + /** + * Converts the rule into a string representation. + * @return string Converts the rule to string + */ + public function toString(): string + { + return $this->operator . self::RULE_SEPARATOR . $this->value; + } + } +} + diff --git a/includes/order-rules/rule-set.php b/includes/order-rules/rule-set.php new file mode 100644 index 0000000..da4e1b7 --- /dev/null +++ b/includes/order-rules/rule-set.php @@ -0,0 +1,116 @@ + + + fields = array_reduce($fieldStrings, function ($fields, $field) { + $value = explode(self::VALUE_SEPARATOR, $field); + $fieldName = $value[0]; + $fieldRule = new TRUNKRS_WC_OrderRule($value[1]); + + ?> + + + order->get_id()); + + try { + if (!isset($this->fields)) + return false; + + $shipping = TRUNKRS_WC_Utils::firstInIterable($wrapper->order->get_items('shipping'))->get_data(); + $data = $wrapper->order->get_data(); + + foreach ($this->fields as $field => $rules) { + $value = key_exists($field, $data) ? $data[$field] : null; + if (!isset($value)) + $value = key_exists($field, $shipping) ? $shipping[$field] : null; + if (!isset($value)) + $value = $wrapper->order->get_meta($field); + + if (!isset($value)) { + return false; + } + + foreach ($rules as $rule) { + $matches = $rule->matches($value); + $auditLog->createEntry($field, $value)->setResult($rule, $matches); + + if (!$matches) { + return false; + } + } + } + + return true; + } finally { + if ($withLog) { + $auditLog->saveLog(); + } + } + } + + /** + * Converts the rule set into a string representation. + * @return string The converted string representation. + */ + public function toString(): string + { + $mappedFields = array_map(function ($rules, $fieldName) { + $mappedRules = array_map(function ($rule) use ($fieldName) { + return $fieldName . self::VALUE_SEPARATOR . $rule->toString(); + }, $rules); + + return implode(self::VALUE_SEPARATOR, $mappedRules); + }, $this->fields, array_keys($this->fields)); + + return implode(self::FIELD_SEPARATOR, $mappedFields); + } + } +} + diff --git a/includes/settings.php b/includes/settings.php index 7104557..7e4b364 100644 --- a/includes/settings.php +++ b/includes/settings.php @@ -3,8 +3,8 @@ if (!class_exists('TRUNKRS_WC_Settings')) { class TRUNKRS_WC_Settings { - const BASE_URL = 'https://shipping.trunkrs.app'; - const TRACK_TRACE_BASE_URL = 'https://parcel.trunkrs.nl/'; + const BASE_URL = 'https://staging.shipping.trunkrs.app'; + const TRACK_TRACE_BASE_URL = 'https://parcel-v2-staging.trunkrs.app/'; const API_VERSION = 'v1'; const OPTION_KEY = 'wc_tr_plugin-settings'; @@ -64,6 +64,15 @@ public static function isConfigured(): bool return self::getSingleOption('isConfigured') ?? false; } + /** + * Reflects whether the order rule engine is enabled. + * @return bool Value reflecting whether rule engine is enabled. + */ + public static function isRuleEngineEnabled(): bool + { + return self::getSingleOption('isRuleEngineEnabled') ?? false; + } + /** * Retrieves the integration details for the current configuration. * @return array The integration details. @@ -104,7 +113,8 @@ public static function getUseDark(): bool * Gets whether to enable track & trace links in order confirmation emails. * @return bool Flag whether to enable track & trace links. */ - public static function getUseTrackTraceLinks(): bool { + public static function getUseTrackTraceLinks(): bool + { return self::getSingleOption('useTrackTraceLinks') ?? false; } @@ -112,7 +122,8 @@ public static function getUseTrackTraceLinks(): bool { * Gets whether to enable track & trace actions in the my account page. * @return bool Flag whether to enable track & trace account actions. */ - public static function getUseAccountActions(): bool { + public static function getUseAccountActions(): bool + { return self::getSingleOption('useTrackTraceActions') ?? false; } @@ -120,10 +131,29 @@ public static function getUseAccountActions(): bool { * Gets whether to enable all orders are for trunkrs on customer checkout. * @return bool Flag whether to enable all orders are for trunkrs. */ - public static function getUseAllOrdersAreTrunkrsActions(): bool { + public static function getUseAllOrdersAreTrunkrsActions(): bool + { return self::getSingleOption('useAllOrdersAreTrunkrs') ?? false; } + /** + * Retrieves the order rule set in string form. + * @return string|null The order set serialized into a string. + */ + public static function getOrderRuleSet() + { + return self::getSingleOption('orderRules') ?? ''; + } + + /** + * Gets whether to use the Subscription plugin renewals as shipment announcements. + * @return bool Flag whether to enable subscription renewal shipments. + */ + public static function getUseSubscriptionRenewals(): bool + { + return self::getSingleOption('useSubRenewals') ?? false; + } + /** * Sets the flag whether the plugin has been configured. * @param $isConfigured bool Flag showing whether the plugin was configured. @@ -133,6 +163,24 @@ public static function setConfigured(bool $isConfigured) self::pushOption('isConfigured', $isConfigured); } + /** + * Saves whether the rules engine is enabled. + * @param bool $enabled Sets whether the order rule engine is enabled. + */ + public static function setIsRuleEngineEnabled(bool $enabled) + { + self::pushOption('isRuleEngineEnabled', $enabled); + } + + /** + * Saves the order rule set. + * @param string $orderRuleSet The order rule set. + */ + public static function setRules(string $orderRuleSet) + { + self::pushOption('orderRules', $orderRuleSet); + } + /** * Saves the integrations details into the store. * @param array $details The integration details. @@ -173,7 +221,8 @@ public static function setUseDark(bool $isUseDark) * Sets whether to enable track & trace links in order confirmation emails. * @param bool $isUseEmailLink */ - public static function setUseEmailLink(bool $isUseEmailLink) { + public static function setUseEmailLink(bool $isUseEmailLink) + { self::pushOption('useTrackTraceLinks', $isUseEmailLink); } @@ -181,7 +230,8 @@ public static function setUseEmailLink(bool $isUseEmailLink) { * Sets whether to enable track & trace actions in the my account area. * @param bool $isUseAccountAction */ - public static function setUseAccountActions(bool $isUseAccountAction) { + public static function setUseAccountActions(bool $isUseAccountAction) + { self::pushOption('useTrackTraceActions', $isUseAccountAction); } @@ -189,9 +239,20 @@ public static function setUseAccountActions(bool $isUseAccountAction) { * Sets whether to enable all orders are for trunkrs on customer checkout. * @param bool $useAllOrdersAreTrunkrs */ - public static function setUseAllOrdersAreTrunkrs(bool $useAllOrdersAreTrunkrs) { + public static function setUseAllOrdersAreTrunkrs(bool $useAllOrdersAreTrunkrs) + { self::pushOption('useAllOrdersAreTrunkrs', $useAllOrdersAreTrunkrs); } + + /** + * Sets whether to use subscription renewals to announce new shipments. + * @param bool $useSubRenewals + * @return void + */ + public static function setUseSubscriptionRenewals(bool $useSubRenewals) + { + self::pushOption('useSubRenewals', $useSubRenewals); + } } diff --git a/includes/utils.php b/includes/utils.php index c69997a..769de65 100644 --- a/includes/utils.php +++ b/includes/utils.php @@ -20,6 +20,19 @@ public static function findInArray(array $array, callable $predicate) return null; } + /** + * Find the first value in the iterable. + * @param $iterable The iterable to evaluate. + * @return mixed|null The first value. + */ + public static function firstInIterable($iterable) + { + foreach ($iterable as $entry) + return $entry; + + return null; + } + /** * Retrieves the base url for assets. * @return string The base url for assets. @@ -88,6 +101,11 @@ public static function parse8601Date($dateString) return $result; } + public static function format8601Date($date): string + { + return $date->format('Y-m-d'); + } + /** * Gets the metadata value from the shipping item. * @param mixed $shippingItem The WC_Order_Item_Shipping item to find the metadata within. @@ -107,3 +125,4 @@ public static function getMetaDataValue($shippingItem, string $key) } } } + diff --git a/includes/wc-internal/index.php b/includes/wc-internal/index.php new file mode 100644 index 0000000..3c0249c --- /dev/null +++ b/includes/wc-internal/index.php @@ -0,0 +1,8 @@ +isTrunkrsOrder) return; @@ -61,6 +62,17 @@ public function cancelOrder(string $orderId) $order->cancelShipment(); } } + + public function orderStatusChanged(string $orderId) + { + if (!TRUNKRS_WC_Settings::isConfigured()) + return; + + $order = new TRUNKRS_WC_Order($orderId); + if ($order->isTrunkrsOrder && $order->isAnnounceable()) { + $order->announceShipment(); + } + } } } diff --git a/includes/wc-internal/shipment-tracking.php b/includes/wc-internal/shipment-tracking.php new file mode 100644 index 0000000..88b38bb --- /dev/null +++ b/includes/wc-internal/shipment-tracking.php @@ -0,0 +1,46 @@ +getTimestamp(), + $trackTraceUrl + ); + } + } + + public function __construct() + { + add_filter('wc_shipment_tracking_get_providers', [$this, 'addTrunkrsProvider']); + } + + public function addTrunkrsProvider(array $providers): array + { + $providers['Netherlands'][self::PROVIDER_NAME] = 'https://parcel.trunkrs.nl/barcode/%1$s'; + $providers['Belgium'][self::PROVIDER_NAME] = 'https://parcel.trunkrs.nl/barcode/%1$s'; + + return $providers; + } + } +} + +new TRUNKRS_WC_ShipmentTracking(); diff --git a/includes/wc-internal/subscriptions.php b/includes/wc-internal/subscriptions.php new file mode 100644 index 0000000..faadb0d --- /dev/null +++ b/includes/wc-internal/subscriptions.php @@ -0,0 +1,26 @@ +isTrunkrsOrder) return; + + $newOrder = new TRUNKRS_WC_Order($order); + $newOrder->announceShipment(); + } + } +} + +new TRUNKRS_WC_Subscriptions(); diff --git a/includes/wc-internal/trunkrs-order.php b/includes/wc-internal/trunkrs-order.php index ba2e2e2..451226f 100644 --- a/includes/wc-internal/trunkrs-order.php +++ b/includes/wc-internal/trunkrs-order.php @@ -3,6 +3,9 @@ if (!class_exists('TRUNKRS_WC_Order')) { class TRUNKRS_WC_Order { + public const TYCHE_DELIVERY_DATE_KEY = 'Delivery Date'; + public const TYCHE_DELIVERY_TIMESTAMP_KEY = '_orddd_lite_timestamp'; + public const DELIVERY_DATE_KEY = 'deliveryDate'; public const CUT_OFF_TIME_KEY = 'cutOffTime'; @@ -36,49 +39,97 @@ class TRUNKRS_WC_Order */ var $isAnnounceFailed = true; + /** + * @var array|string|false The order meta-data related to the Trunkrs integration. + */ + var $orderMeta; + /** * @param $orderOrOrderId int|WC_Order The WooCommerce order instance. * @param bool $withMeta bool Flag whether to also parse meta data details. */ - public function __construct($orderOrOrderId, $withMeta = true) + public function __construct($orderOrOrderId, bool $withMeta = true, bool $withLogging = false) { $this->order = $orderOrOrderId instanceof WC_Order ? $orderOrOrderId : new WC_Order($orderOrOrderId); - $this->init($withMeta); + + $this->init($withMeta, $withLogging); } - private function init($withMeta) + private function isTrunkrsOrder(bool $withLogging) { - $shippingItem = $this->order->get_items('shipping'); + if (!empty($this->orderMeta) && is_array($this->orderMeta)) { + return true; + } + + if (TRUNKRS_WC_Settings::getUseAllOrdersAreTrunkrsActions()) { + return true; + } + if (TRUNKRS_WC_Settings::isRuleEngineEnabled()) { + $ruleSet = new TRUNKRS_WC_RuleSet(TRUNKRS_WC_Settings::getOrderRuleSet()); + return $ruleSet->matchOrder($this, $withLogging); + } + + $shippingItem = $this->order->get_items('shipping'); foreach ($shippingItem as $item) { $shippingMethodId = $item->get_method_id(); - if (!$this->isAllOrdersForTrunkrs() - && $shippingMethodId !== TRUNKRS_WC_Bootstrapper::DOMAIN) { - continue; + if ($shippingMethodId === TRUNKRS_WC_Bootstrapper::DOMAIN) { + return true; } + } - $this->isTrunkrsOrder = true; - if (!$withMeta) - return; + return false; + } - $meta = get_post_meta($this->order->get_id(), TRUNKRS_WC_Bootstrapper::DOMAIN, true); - if (empty($meta) || !is_array($meta)) - return; + private function init(bool $withMeta, bool $withLogging = false) + { + $this->orderMeta = get_post_meta($this->order->get_id(), TRUNKRS_WC_Bootstrapper::DOMAIN, true); + $this->isTrunkrsOrder = $this->isTrunkrsOrder($withLogging); - $this->trunkrsNr = $meta['trunkrsNr']; - $this->deliveryDate = $meta['deliveryDate']; - $this->isCancelled = $meta['isCanceled']; - $this->isAnnounceFailed = $meta['isAnnounceFailed']; + if (!$this->isTrunkrsOrder || !$withMeta) { + return; } + + // When no meta is available stop processing + if (empty($this->orderMeta) || !is_array($this->orderMeta)) + return; + + $this->trunkrsNr = $this->orderMeta['trunkrsNr']; + $this->deliveryDate = $this->orderMeta['deliveryDate']; + $this->isCancelled = $this->orderMeta['isCanceled']; + $this->isAnnounceFailed = $this->orderMeta['isAnnounceFailed']; + } + + private function getDeliveryDate($item) + { + $deliveryDatePlugin = $this->order->get_meta(self::TYCHE_DELIVERY_TIMESTAMP_KEY); + + if (isset($deliveryDatePlugin)) { + $parsed = DateTime::createFromFormat('U', $deliveryDatePlugin); + if ($parsed === false) return $item->get_meta(self::DELIVERY_DATE_KEY); + return TRUNKRS_WC_Utils::format8601Date($parsed); + } + + return $item->get_meta(self::DELIVERY_DATE_KEY); + } + + /** + * Checks whether this order can be announced in its current state. + * @return bool Value representing whether this order can be announced. + */ + public function isAnnounceable(): bool + { + return !isset($this->trunkrsNr) || $this->isAnnounceFailed || $this->isCancelled; } /** * Formats the delivery date in a human-readable format. * @return string The formatted delivery date. */ - public function getFormattedDate(): string { + public function getFormattedDate(): string + { if (empty($this->deliveryDate)) return ''; @@ -90,7 +141,8 @@ public function getFormattedDate(): string { * Retrieves whether this order has an available Track&Trace link. * @return bool Flag whether track&trace link is available. */ - public function isTrackTraceAvailable(): bool { + public function isTrackTraceAvailable(): bool + { return $this->isTrunkrsOrder && !$this->isAnnounceFailed; } @@ -98,53 +150,56 @@ public function isTrackTraceAvailable(): bool { * Creates a track&trace link for this order's shipment. * @return string The track&trace link. */ - public function getTrackTraceLink(): string { + public function getTrackTraceLink(): string + { $postalCode = $this->order->get_shipping_postcode(); return TRUNKRS_WC_Settings::TRACK_TRACE_BASE_URL . $this->trunkrsNr . '/' . $postalCode; } - /** - * @return bool Flag whether all orders are for Trunkrs - */ - public function isAllOrdersForTrunkrs(): bool { - return TRUNKRS_WC_Settings::getUseAllOrdersAreTrunkrsActions(); - } - /** * Announces the order as a new shipment to the Trunkrs API. */ - public function announceShipment() { - $shippingItems = $this->order->get_items('shipping'); - - foreach ($shippingItems as $item) { - $deliveryDate = TRUNKRS_WC_Utils::getMetaDataValue($item, self::DELIVERY_DATE_KEY); + public function announceShipment() + { + if (!$this->isAnnounceable()) return; - $reference = sprintf('%s-%s', uniqid(), $this->order->get_id()); - $shipment = TRUNKRS_WC_Api::announceShipment($this->order, $reference, $deliveryDate); + $shippingItem = TRUNKRS_WC_Utils::firstInIterable($this->order->get_items('shipping')); + $deliveryDate = $this->getDeliveryDate($shippingItem); - $item->delete_meta_data(self::DELIVERY_DATE_KEY); - $item->delete_meta_data(self::CUT_OFF_TIME_KEY); + $reference = $this->order->get_order_key(); + $shipment = TRUNKRS_WC_Api::announceShipment($this->order, $reference, $deliveryDate); - $item->save_meta_data(); + $shippingItem->delete_meta_data(self::DELIVERY_DATE_KEY); + $shippingItem->delete_meta_data(self::CUT_OFF_TIME_KEY); - if (is_null($shipment)) { - $this->isAnnounceFailed = true; - $this->save(); - return; - } + $shippingItem->save_meta_data(); - $this->trunkrsNr = $shipment->trunkrsNumber; - $this->deliveryDate = $shipment->deliveryWindow->date; - $this->isCancelled = false; - $this->isAnnounceFailed = false; + if (is_null($shipment)) { + $this->isAnnounceFailed = true; $this->save(); + return; } + + $this->trunkrsNr = $shipment->trunkrsNumber; + $this->deliveryDate = $shipment->deliveryWindow->date; + $this->isCancelled = false; + $this->isAnnounceFailed = false; + + TRUNKRS_WC_ShipmentTracking::setTrackingInfo( + $this->order->get_id(), + $this->trunkrsNr, + $this->getTrackTraceLink(), + $this->deliveryDate + ); + + $this->save(); } /** * Cancels the shipment attached to this order. */ - public function cancelShipment() { + public function cancelShipment() + { $isSuccess = TRUNKRS_WC_Api::cancelShipment($this->trunkrsNr); if ($isSuccess) { @@ -170,6 +225,16 @@ public function save() 'isAnnounceFailed' => $this->isAnnounceFailed, ]; + $currentPluginDate = $this->order->get_meta(self::TYCHE_DELIVERY_DATE_KEY); + if (!$this->isAnnounceFailed && !empty($currentPluginDate)) { + $dateValue = TRUNKRS_WC_Utils::parse8601($this->deliveryDate); + $dateString = $dateValue->format('d F, Y'); + $dateStamp = $dateValue->getTimestamp(); + + update_post_meta($this->order->get_id(), self::TYCHE_DELIVERY_DATE_KEY, $dateString); + update_post_meta($this->order->get_id(), self::TYCHE_DELIVERY_TIMESTAMP_KEY, $dateStamp); + } + $currentValue = get_post_meta($this->order->get_id(), TRUNKRS_WC_Bootstrapper::DOMAIN, true); if (empty($currentValue) || !is_array($currentValue)) add_post_meta($this->order->get_id(), TRUNKRS_WC_Bootstrapper::DOMAIN, $metaValue); diff --git a/package.json b/package.json index 013d476..50e11a8 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,10 @@ "@wordpress/element": "^3.1.1", "auth0-js": "^9.16.2", "axios": "^0.21.1", - "clsx": "^1.1.1" + "clsx": "^1.1.1", + "formik": "^2.2.9", + "react-event-listener": "^0.6.6", + "yup": "^0.32.11" }, "devDependencies": { "@babel/plugin-proposal-class-properties": "^7.14.5", @@ -17,6 +20,7 @@ "@babel/preset-react": "^7.14.5", "@svgr/webpack": "^5.5.0", "@types/auth0-js": "^9.14.4", + "@types/react-event-listener": "^0.4.12", "@types/wordpress__blocks": "^9.0.0", "@types/wordpress__components": "^14.0.0", "@typescript-eslint/eslint-plugin": "^4.0.1", diff --git a/readme.txt b/readme.txt index 0d189a0..63c290f 100644 --- a/readme.txt +++ b/readme.txt @@ -2,8 +2,8 @@ Contributors: fean Tags: delivery, packages, woocommerce, trunkrs, sameday, delivery Requires at least: 3.6 & WooCommerce 3.0+ -Tested up to: 5.8 -Stable tag: 1.1.1 +Tested up to: 5.9 +Stable tag: 1.2.0 Requires PHP: 7.1 License: GPLv3 License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -41,6 +41,14 @@ Reach out to your customer success manager or account manager to get started, th == Changelog == += 1.2.0 = +This is a big release with lots of new and cool features you will love! +We added the following features: +- Support for order filter rules that let you choose which orders should go to Trunkrs. +- Support for Tyche Delivery Date plugin for custom delivery dates. +- Support for WooCommerce Shipment Tracking extension. +- Support for WooCommerce Subscriptions extension. + = 1.1.0 = This release contains more settings to adjust the way we show up in your cart and checkout area. It's now also possible to redirect all orders to the Trunkrs shipping service. diff --git a/trunkrs-woocommerce.php b/trunkrs-woocommerce.php index bc76632..8bed2ad 100644 --- a/trunkrs-woocommerce.php +++ b/trunkrs-woocommerce.php @@ -5,7 +5,7 @@ * Description: Add excellent consumer focused shipping to your WooCommerce store. * Author: Trunkrs * Author URI: https://trunkrs.nl - * Version: 1.1.1 + * Version: 1.2.0 * Requires at least: 3.6 & WooCommerce 3.0+ * Requires PHP: 7.1 * License: GPLv3 @@ -68,6 +68,7 @@ public function __construct() add_action('plugins_loaded', [$this, 'loadTranslations']); add_action('init', [$this, 'loadMain']); + register_activation_hook(__FILE__, [$this, 'loadTables']); } public function notifyWooCommerce() @@ -148,24 +149,8 @@ private function define($name, $value) private function loadClasses() { - // Autoload the vendor packages - require_once($this->pluginPath . '/vendor/autoload.php'); - - // Load internal classes $includePath = $this->pluginPath . '/includes'; - - require_once($includePath . '/settings.php'); - require_once($includePath . '/api.php'); - - require_once($includePath . '/wc-internal/trunkrs-order.php'); - require_once($includePath . '/wc-internal/orders.php'); - require_once($includePath . '/wc-internal/shipping-method.php'); - require_once($includePath . '/wc-internal/notices.php'); - require_once($includePath . '/wc-internal/track-trace.php'); - - require_once($includePath . '/admin/admin-page.php'); - require_once($includePath . '/admin/admin-order-page.php'); - require_once($includePath . '/admin/admin-endpoints.php'); + require_once($includePath . '/index.php'); } public function loadTranslations() { @@ -183,6 +168,11 @@ public function loadTranslations() { load_plugin_textdomain(TRUNKRS_WC_Bootstrapper::DOMAIN, false, $pluginTextDomainFile); } + public function loadTables() { + require_once($this->pluginPath . '/includes/init-db.php'); + TRUNKRS_WC_InitDB::init(); + } + private function isWooCommerceActive() { $blog_plugins = get_option('active_plugins', []); diff --git a/yarn.lock b/yarn.lock index d4067ca..df45f83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -936,6 +936,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.15.4", "@babel/runtime@^7.2.0": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" + integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.14.5", "@babel/template@^7.3.3": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4" @@ -1521,6 +1528,11 @@ dependencies: "@types/node" "*" +"@types/lodash@^4.14.175": + version "4.14.178" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" + integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw== + "@types/mdast@^3.0.0": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb" @@ -1580,6 +1592,13 @@ dependencies: "@types/react" "^16" +"@types/react-event-listener@^0.4.12": + version "0.4.12" + resolved "https://registry.yarnpkg.com/@types/react-event-listener/-/react-event-listener-0.4.12.tgz#601b6b83e53fbea191cadf32175e4d9af6004e64" + integrity sha512-ilfwCBSnIfd55qhMTIYvEjoMVds8xXDgwoBd1CT1MjiZBmE8oEstjrHos5iI/pUmAGooalo9oEUBc79kXVuYlA== + dependencies: + "@types/react" "*" + "@types/react@*": version "17.0.13" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.13.tgz#6b7c9a8f2868586ad87d941c02337c6888fb874f" @@ -4119,6 +4138,11 @@ deep-is@^0.1.3, deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +deepmerge@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" + integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== + deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" @@ -5450,6 +5474,19 @@ formidable@^1.2.2: resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== +formik@^2.2.9: + version "2.2.9" + resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.9.tgz#8594ba9c5e2e5cf1f42c5704128e119fc46232d0" + integrity sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA== + dependencies: + deepmerge "^2.1.1" + hoist-non-react-statics "^3.3.0" + lodash "^4.17.21" + lodash-es "^4.17.21" + react-fast-compare "^2.0.1" + tiny-warning "^1.0.2" + tslib "^1.10.0" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -5891,6 +5928,13 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoist-non-react-statics@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -7376,6 +7420,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -7458,7 +7507,7 @@ longest-streak@^2.0.0: resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== -loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -7988,6 +8037,11 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== +nanoclone@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" + integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -9011,6 +9065,15 @@ prop-types-exact@^1.2.0: object.assign "^4.1.0" reflect.ownkeys "^0.2.0" +prop-types@^15.6.0: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -9020,6 +9083,11 @@ prop-types@^15.6.2, prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" +property-expr@^2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4" + integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA== + proxy-addr@~2.0.5: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -9253,7 +9321,21 @@ react-dom@^16.13.1: prop-types "^15.6.2" scheduler "^0.19.1" -react-is@^16.12.0, react-is@^16.13.1, react-is@^16.8.1, react-is@^16.8.6: +react-event-listener@^0.6.6: + version "0.6.6" + resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.6.6.tgz#758f7b991cad9086dd39fd29fad72127e1d8962a" + integrity sha512-+hCNqfy7o9wvO6UgjqFmBzARJS7qrNoda0VqzvOuioEpoEXKutiKuv92dSz6kP7rYLmyHPyYNLesi5t/aH1gfw== + dependencies: + "@babel/runtime" "^7.2.0" + prop-types "^15.6.0" + warning "^4.0.1" + +react-fast-compare@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" + integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== + +react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -10797,6 +10879,11 @@ tiny-lr@^1.1.1: object-assign "^4.1.0" qs "^6.4.0" +tiny-warning@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -10861,6 +10948,11 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= + tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -10936,7 +11028,7 @@ tsconfig-paths@^3.9.0: minimist "^1.2.0" strip-bom "^3.0.0" -tslib@^1.8.1, tslib@^1.9.0: +tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -11431,6 +11523,13 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.x" +warning@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + watchpack-chokidar2@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957" @@ -11834,6 +11933,19 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +yup@^0.32.11: + version "0.32.11" + resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5" + integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg== + dependencies: + "@babel/runtime" "^7.15.4" + "@types/lodash" "^4.14.175" + lodash "^4.17.21" + lodash-es "^4.17.21" + nanoclone "^0.2.1" + property-expr "^2.0.4" + toposort "^2.0.2" + zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"