commit aa223ce3bb86c226c3295b0dfe0c2d889f41e1e6 Author: tieptk Date: Sat Mar 7 10:26:20 2026 +0700 u diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..6468471 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,44 @@ +{ + "permissions": { + "allow": [ + "Bash(powershell.exe -Command \"composer --version\")", + "Bash(php -l data/menu_converted.php)", + "Bash(mv data/menu.php data/menu_backup.php)", + "Bash(mv data/menu_converted.php data/menu.php)", + "Bash(rm _convert_menu.php)", + "mcp__claude_ai_Figma__get_design_context", + "mcp__claude_ai_Figma__get_screenshot", + "WebFetch(domain:local.agent_test)", + "mcp__claude_ai_Figma__get_metadata", + "Bash(node -e \":*)", + "Bash(node -e \"const fs=require\\(''fs''\\);const c=fs.readFileSync\\(''C:/Users/APC/.claude/projects/c--Users-APC-Downloads-work-agent-test/bf9da1e9-c8e0-4b85-88e6-47358abbfdcc/tool-results/mcp-claude_ai_Figma-get_metadata-1772848492772.txt'',''utf8''\\);const d=JSON.parse\\(c\\);const t=d[0].text;const ls=t.split\\(''\\\\n''\\);ls.forEach\\(\\(l,i\\)=>{if\\(/menu|danh|category|showroom|dropdown|nav|open/i.test\\(l\\)\\){console.log\\(i+'': ''+l.substring\\(0,200\\)\\)}}\\);\")", + "Bash(node -e \"const fs=require\\(''fs''\\);const c=fs.readFileSync\\(''C:/Users/APC/.claude/projects/c--Users-APC-Downloads-work-agent-test/bf9da1e9-c8e0-4b85-88e6-47358abbfdcc/tool-results/mcp-claude_ai_Figma-get_metadata-1772848492772.txt'',''utf8''\\);const d=JSON.parse\\(c\\);const t=d[0].text;const ls=t.split\\(''\\\\n''\\);// Print top-level frames \\(page children\\) - lines with frame id at low indent:*)", + "Bash(curl -s -o menu_may_lanh.png \"https://www.figma.com/api/mcp/asset/4e4e0480-d097-48a5-9320-5ada2d995706\")", + "Bash(curl -s -o menu_tu_lanh.png \"https://www.figma.com/api/mcp/asset/bd563be6-4c28-483e-9509-85e10803ca3d\")", + "Bash(curl -s -o menu_tivi.png \"https://www.figma.com/api/mcp/asset/39ada317-6fbf-4b16-975c-8a28c0478840\")", + "Bash(curl -s -o menu_may_say.png \"https://www.figma.com/api/mcp/asset/1b571249-0bca-47c5-8cf4-779533548d41\")", + "Bash(curl -s -o menu_may_giat.png \"https://www.figma.com/api/mcp/asset/70d4c142-e402-4567-b164-afe22ecb5294\")", + "Bash(curl -s -o menu_may_nuoc_nong.png \"https://www.figma.com/api/mcp/asset/7d029f58-802c-47c1-a83f-55469ffe509c\")", + "Bash(curl -s -o menu_tu_mat.png \"https://www.figma.com/api/mcp/asset/c649befb-7cbf-4d9c-9aa9-69384a1f2eb9\")", + "Bash(curl -s -o menu_tu_dong.png \"https://www.figma.com/api/mcp/asset/50768d63-9f95-476e-bbc7-8c3f51262302\")", + "Bash(curl -s -o menu_may_rua_bat.png \"https://www.figma.com/api/mcp/asset/58f0f14d-62b1-4a6f-8b84-67f1465810a9\")", + "Bash(curl -s -o brand_samsung.png \"https://www.figma.com/api/mcp/asset/e9059f0c-1e4a-4e10-856e-4320bd765832\")", + "Bash(curl -s -o brand_lg.png \"https://www.figma.com/api/mcp/asset/c62f6078-b073-4d3e-aec4-ba8f020dbc22\")", + "Bash(curl -s -o brand_sony.png \"https://www.figma.com/api/mcp/asset/fb3e3b33-fc5e-442c-8cd4-2766485f9065\")", + "Bash(curl -s -o brand_toshiba.png \"https://www.figma.com/api/mcp/asset/72eb8f5d-5b45-47c8-b0a6-0dcc3ac6442a\")", + "Bash(curl -s -o brand_aqua.png \"https://www.figma.com/api/mcp/asset/b3d2dde1-7b8c-4a77-9f64-e2ef9664bfa5\")", + "Bash(curl -s -o brand_tcl.png \"https://www.figma.com/api/mcp/asset/b8fb0d87-7e7c-441b-923c-5483afec26cc\")", + "Bash(curl -s -o icon_fire.png \"https://www.figma.com/api/mcp/asset/5376c62c-720c-487a-b6b7-e2a6f241b216\")", + "Bash(curl -s -o icon_sale_badge.png \"https://www.figma.com/api/mcp/asset/730adacb-889a-4fcf-920e-81adc7dc9387\")", + "Bash(curl -s -o icon_chevron_right_menu.svg \"https://www.figma.com/api/mcp/asset/06ec1348-1661-4d1d-9f2b-6df1628f6a89\")", + "Bash(curl -s -o icon_map_pin.svg \"https://www.figma.com/api/mcp/asset/3b6002b1-280d-44c9-97f7-752fbbdc748e\")", + "Bash(curl -s -o icon_phone.svg \"https://www.figma.com/api/mcp/asset/d01881d5-28c5-49c0-8587-878902b8a532\")", + "Bash(curl -s -o icon_support.svg \"https://www.figma.com/api/mcp/asset/da2be7e1-3c3c-4b9e-8fc7-8e7c70208cb8\")", + "Bash(curl -s -o icon_map_fill.svg \"https://www.figma.com/api/mcp/asset/6b1b28b3-d6bc-45fe-a2b8-f6d2855f96b9\")", + "Bash(curl -L \"https://www.figma.com/api/mcp/asset/f8150959-3677-4d5c-85d9-70310533fb75\" -o \"c:/Users/APC/Downloads/work/agent_test/export_to_html/image/search_promo_banner.png\" --silent)", + "WebFetch(domain:www.figma.com)", + "Bash(curl -s -H \"X-Figma-Token: figd_uiWTwLaZj6DQBU16r7t9r8Ei9zOhD-fevGWEBPwp\" \"https://api.figma.com/v1/files/JmbJxl6r2KMggCtiJOc0gZ/nodes?ids=7-6456\")", + "Bash(python -m json.tool)" + ] + } +} diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..fbca9d3 --- /dev/null +++ b/.htaccess @@ -0,0 +1,7 @@ +RewriteEngine on +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_URI} !^/index.php +# prevent rewrite non-existent files +RewriteCond %{REQUEST_URI} !\.(jpg|png|gif|css|js|php|tiff|jpeg|ico)$ +RewriteRule ^(.*)$ /index.php [QSA,L] \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8df28bd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "chat.mcp.discovery.enabled": { + "claude-desktop": true + }, + "chat.viewProgressBadge.enabled": true +} \ No newline at end of file diff --git a/Máy b/Máy new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d1d997 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +

Hướng dẫn

+ +

Link thiết kế: Giao diện Admin

+ +

Repo: https://repo.hurasoft.com/tieptk/admin_hura_8

+ +

Theo dõi tiến độ: https://docs.google.com/spreadsheets/d/1Po3ANsG00pm_Y3dnrwuV81cidTCCqepYmR3yPDlIq6c/edit#gid=0

+ +

Cài đặt hệ thống

+ +

Test và làm việc chính tại web: http://local.hura8_admin/

+ + + + + +

Cấu trúc thư mục

+ + + +

Cài đặt Composer (dùng tải package của PHP)

+ +

Xem hướng dẫn: https://getcomposer.org/doc/00-intro.md#installation-windows

+ +

Sau khi cài đặt xong. Mở cmd của Windows và thao tác lệnh sau để cài các thư viện code PHP cần cho dự án này.

+ +

+    > cd /thuc-muc-check-out/admin_hura_8/package
+    > composer i 
+
+ + + +

Sử dụng Tailwind để style giao diện.

+ + + diff --git a/_shared.php b/_shared.php new file mode 100644 index 0000000..6f727f1 --- /dev/null +++ b/_shared.php @@ -0,0 +1,17 @@ + .drawer-overlay { + position: sticky; + top: calc(0.25rem * 0); + cursor: pointer; + place-self: stretch; + background-color: oklch(0% 0 0 / 40%); + } + > * { + grid-column-start: 1; + grid-row-start: 1; + } + > *:not(.drawer-overlay) { + will-change: transform; + transition: translate 0.3s ease-out, width 0.2s ease-out; + translate: -100%; + [dir="rtl"] & { + translate: 100%; + } + } + } + } + .drawer-toggle { + @layer daisyui.l1.l2.l3 { + position: fixed; + height: calc(0.25rem * 0); + width: calc(0.25rem * 0); + appearance: none; + opacity: 0%; + :where(&:checked ~ .drawer-side) { + scrollbar-color: currentColor oklch(0 0 0 / calc(var(--page-has-backdrop, 0) * 0.4)); + @supports (color: color-mix(in lab, red, red)) { + scrollbar-color: color-mix(in oklch, currentColor 35%, #0000) oklch(0 0 0 / calc(var(--page-has-backdrop, 0) * 0.4)); + } + } + :where(:root:has(&:checked)) { + --page-has-backdrop: 1; + --page-overflow: hidden; + --page-scroll-bg: var(--page-scroll-bg-on); + --page-scroll-gutter: stable; + --page-scroll-transition: var(--page-scroll-transition-on); + animation: set-page-has-scroll forwards; + animation-timeline: scroll(); + } + } + @layer daisyui.l1.l2 { + :where(&:checked ~ .drawer-side) { + pointer-events: auto; + visibility: visible; + overflow-y: auto; + opacity: 100%; + & > *:not(.drawer-overlay) { + translate: 0%; + } + } + &:focus-visible ~ .drawer-content label.drawer-button { + outline: 2px solid; + outline-offset: 2px; + } + } + } + .tooltip { + @layer daisyui.l1.l2.l3 { + position: relative; + display: inline-block; + --tt-bg: var(--color-neutral); + --tt-off: calc(100% + 0.5rem); + --tt-tail: calc(100% + 1px + 0.25rem); + & > .tooltip-content, &[data-tip]:before { + position: absolute; + max-width: 20rem; + border-radius: var(--radius-field); + padding-inline: calc(0.25rem * 2); + padding-block: calc(0.25rem * 1); + text-align: center; + white-space: normal; + color: var(--color-neutral-content); + opacity: 0%; + font-size: 0.875rem; + line-height: 1.25; + background-color: var(--tt-bg); + width: max-content; + pointer-events: none; + z-index: 2; + --tw-content: attr(data-tip); + content: var(--tw-content); + } + &:after { + opacity: 0%; + background-color: var(--tt-bg); + content: ""; + pointer-events: none; + width: 0.625rem; + height: 0.25rem; + display: block; + position: absolute; + mask-repeat: no-repeat; + mask-position: -1px 0; + --mask-tooltip: url("data:image/svg+xml,%3Csvg width='10' height='4' viewBox='0 0 8 4' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.500009 1C3.5 1 3.00001 4 5.00001 4C7 4 6.5 1 9.5 1C10 1 10 0.499897 10 0H0C-1.99338e-08 0.5 0 1 0.500009 1Z' fill='black'/%3E%3C/svg%3E%0A"); + mask-image: var(--mask-tooltip); + } + @media (prefers-reduced-motion: no-preference) { + & > .tooltip-content, &[data-tip]:before, &:after { + transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms; + } + } + &:is([data-tip]:not([data-tip=""]), :has(.tooltip-content:not(:empty))) { + &.tooltip-open, &:hover, &:has(:focus-visible) { + & > .tooltip-content, &[data-tip]:before, &:after { + opacity: 100%; + --tt-pos: 0rem; + @media (prefers-reduced-motion: no-preference) { + transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0s, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0s; + } + } + } + } + } + @layer daisyui.l1.l2 { + > .tooltip-content, &[data-tip]:before { + transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem)); + inset: auto auto var(--tt-off) 50%; + } + &:after { + transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem)); + inset: auto auto var(--tt-tail) 50%; + } + } + } + .tab { + @layer daisyui.l1.l2.l3 { + position: relative; + display: inline-flex; + cursor: pointer; + appearance: none; + flex-wrap: wrap; + align-items: center; + justify-content: center; + text-align: center; + webkit-user-select: none; + user-select: none; + &:hover { + @media (hover: hover) { + color: var(--color-base-content); + } + } + --tab-p: 0.75rem; + --tab-bg: var(--color-base-100); + --tab-border-color: var(--color-base-300); + --tab-radius-ss: 0; + --tab-radius-se: 0; + --tab-radius-es: 0; + --tab-radius-ee: 0; + --tab-order: 0; + --tab-radius-min: calc(0.75rem - var(--border)); + --tab-radius-limit: min(var(--radius-field), var(--tab-radius-min)); + --tab-radius-grad: #0000 calc(69% - var(--border)), + var(--tab-border-color) calc(69% - var(--border) + 0.25px), + var(--tab-border-color) 69%, + var(--tab-bg) calc(69% + 0.25px); + border-color: #0000; + order: var(--tab-order); + height: var(--tab-height); + font-size: 0.875rem; + padding-inline: var(--tab-p); + &:is(input[type="radio"]) { + min-width: fit-content; + &:after { + --tw-content: attr(aria-label); + content: var(--tw-content); + } + } + &:is(label) { + position: relative; + input { + position: absolute; + inset: calc(0.25rem * 0); + cursor: pointer; + appearance: none; + opacity: 0%; + } + } + &:checked, &:is(label:has(:checked)), &:is(.tab-active, [aria-selected="true"], [aria-current="true"], [aria-current="page"]) { + & + .tab-content { + display: block; + } + } + &:not( :checked, label:has(:checked), :hover, .tab-active, [aria-selected="true"], [aria-current="true"], [aria-current="page"] ) { + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 50%, transparent); + } + } + &:not(input):empty { + flex-grow: 1; + cursor: default; + } + &:focus { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + &:focus-visible, &:is(label:has(:checked:focus-visible)) { + outline: 2px solid currentColor; + outline-offset: -5px; + } + &[disabled] { + pointer-events: none; + opacity: 40%; + } + } + } + .menu { + @layer daisyui.l1.l2.l3 { + display: flex; + width: fit-content; + flex-direction: column; + flex-wrap: wrap; + padding: calc(0.25rem * 2); + --menu-active-fg: var(--color-neutral-content); + --menu-active-bg: var(--color-neutral); + font-size: 0.875rem; + :where(li ul) { + position: relative; + margin-inline-start: calc(0.25rem * 4); + padding-inline-start: calc(0.25rem * 2); + white-space: nowrap; + &:before { + position: absolute; + inset-inline-start: calc(0.25rem * 0); + top: calc(0.25rem * 3); + bottom: calc(0.25rem * 3); + background-color: var(--color-base-content); + opacity: 10%; + width: var(--border); + content: ""; + } + } + :where(li > .menu-dropdown:not(.menu-dropdown-show)) { + display: none; + } + :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), :where(li:not(.menu-title) > details > summary:not(.menu-title)) { + display: grid; + grid-auto-flow: column; + align-content: flex-start; + align-items: center; + gap: calc(0.25rem * 2); + border-radius: var(--radius-field); + padding-inline: calc(0.25rem * 3); + padding-block: calc(0.25rem * 1.5); + text-align: start; + transition-property: color, background-color, box-shadow; + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + grid-auto-columns: minmax(auto, max-content) auto max-content; + text-wrap: balance; + user-select: none; + } + :where(li > details > summary) { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + &::-webkit-details-marker { + display: none; + } + } + :where(li > details > summary), :where(li > .menu-dropdown-toggle) { + &:after { + justify-self: flex-end; + display: block; + height: 0.375rem; + width: 0.375rem; + rotate: -135deg; + translate: 0 -1px; + transition-property: rotate, translate; + transition-duration: 0.2s; + content: ""; + transform-origin: 50% 50%; + box-shadow: 2px 2px inset; + pointer-events: none; + } + } + details { + overflow: hidden; + interpolate-size: allow-keywords; + } + details::details-content { + block-size: 0; + @media (prefers-reduced-motion: no-preference) { + transition-behavior: allow-discrete; + transition-property: block-size, content-visibility; + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + } + } + details[open]::details-content { + block-size: auto; + } + :where(li > details[open] > summary):after, :where(li > .menu-dropdown-toggle.menu-dropdown-show):after { + rotate: 45deg; + translate: 0 1px; + } + :where( li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title), li:not(.menu-title, .disabled) > details > summary:not(.menu-title) ):not(.menu-active, :active, .btn) { + &.menu-focus, &:focus-visible { + cursor: pointer; + background-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-base-content) 10%, transparent); + } + color: var(--color-base-content); + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + } + :where( li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title):not(.menu-active, :active, .btn):hover, li:not(.menu-title, .disabled) > details > summary:not(.menu-title):not(.menu-active, :active, .btn):hover ) { + cursor: pointer; + background-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-base-content) 10%, transparent); + } + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + box-shadow: 0 1px oklch(0% 0 0 / 0.01) inset, 0 -1px oklch(100% 0 0 / 0.01) inset; + } + :where(li:empty) { + background-color: var(--color-base-content); + opacity: 10%; + margin: 0.5rem 1rem; + height: 1px; + } + :where(li) { + position: relative; + display: flex; + flex-shrink: 0; + flex-direction: column; + flex-wrap: wrap; + align-items: stretch; + .badge { + justify-self: flex-end; + } + & > *:not(ul, .menu-title, details, .btn):active, & > *:not(ul, .menu-title, details, .btn).menu-active, & > details > summary:active { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + color: var(--menu-active-fg); + background-color: var(--menu-active-bg); + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + &:not(&:active) { + box-shadow: 0 2px calc(var(--depth) * 3px) -2px var(--menu-active-bg); + } + } + &.menu-disabled { + pointer-events: none; + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 20%, transparent); + } + } + } + .dropdown:focus-within { + .menu-dropdown-toggle:after { + rotate: 45deg; + translate: 0 1px; + } + } + .dropdown-content { + margin-top: calc(0.25rem * 2); + padding: calc(0.25rem * 2); + &:before { + display: none; + } + } + } + } + .dropdown { + @layer daisyui.l1.l2.l3 { + position: relative; + display: inline-block; + position-area: var(--anchor-v, bottom) var(--anchor-h, span-right); + & > *:not(:has(~ [class*="dropdown-content"])):focus { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + .dropdown-content { + position: absolute; + } + &.dropdown-close .dropdown-content, &:not(details, .dropdown-open, .dropdown-hover:hover, :focus-within) .dropdown-content, &.dropdown-hover:not(:hover) [tabindex]:first-child:focus:not(:focus-visible) ~ .dropdown-content { + display: none; + transform-origin: top; + opacity: 0%; + scale: 95%; + } + &[popover], .dropdown-content { + z-index: 999; + @media (prefers-reduced-motion: no-preference) { + animation: dropdown 0.2s; + transition-property: opacity, scale, display; + transition-behavior: allow-discrete; + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + } + } + @starting-style { + &[popover], .dropdown-content { + scale: 95%; + opacity: 0; + } + } + &:not(.dropdown-close) { + &.dropdown-open, &:not(.dropdown-hover):focus, &:focus-within { + > [tabindex]:first-child { + pointer-events: none; + } + .dropdown-content { + opacity: 100%; + scale: 100%; + } + } + &.dropdown-hover:hover { + .dropdown-content { + opacity: 100%; + scale: 100%; + } + } + } + &:is(details) { + summary { + &::-webkit-details-marker { + display: none; + } + } + } + &:where([popover]) { + background: #0000; + } + &[popover] { + position: fixed; + color: inherit; + @supports not (position-area: bottom) { + margin: auto; + &.dropdown-close, &.dropdown-open:not(:popover-open) { + display: none; + transform-origin: top; + opacity: 0%; + scale: 95%; + } + &::backdrop { + background-color: color-mix(in oklab, #000 30%, #0000); + } + } + &.dropdown-close, &:not(.dropdown-open, :popover-open) { + display: none; + transform-origin: top; + opacity: 0%; + scale: 95%; + } + } + } + } + .btn { + :where(&) { + @layer daisyui.l1.l2.l3 { + width: unset; + } + } + @layer daisyui.l1.l2.l3 { + display: inline-flex; + flex-shrink: 0; + cursor: pointer; + flex-wrap: nowrap; + align-items: center; + justify-content: center; + gap: calc(0.25rem * 1.5); + text-align: center; + vertical-align: middle; + outline-offset: 2px; + webkit-user-select: none; + user-select: none; + padding-inline: var(--btn-p); + color: var(--btn-fg); + --tw-prose-links: var(--btn-fg); + height: var(--size); + font-size: var(--fontsize, 0.875rem); + font-weight: 600; + outline-color: var(--btn-color, var(--color-base-content)); + transition-property: color, background-color, border-color, box-shadow; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-duration: 0.2s; + border-start-start-radius: var(--join-ss, var(--radius-field)); + border-start-end-radius: var(--join-se, var(--radius-field)); + border-end-start-radius: var(--join-es, var(--radius-field)); + border-end-end-radius: var(--join-ee, var(--radius-field)); + background-color: var(--btn-bg); + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--btn-noise); + border-width: var(--border); + border-style: solid; + border-color: var(--btn-border); + text-shadow: 0 0.5px oklch(100% 0 0 / calc(var(--depth) * 0.15)); + touch-action: manipulation; + box-shadow: 0 0.5px 0 0.5px oklch(100% 0 0 / calc(var(--depth) * 6%)) inset, var(--btn-shadow); + --size: calc(var(--size-field, 0.25rem) * 10); + --btn-bg: var(--btn-color, var(--color-base-200)); + --btn-fg: var(--color-base-content); + --btn-p: 1rem; + --btn-border: var(--btn-bg); + @supports (color: color-mix(in lab, red, red)) { + --btn-border: color-mix(in oklab, var(--btn-bg), #000 calc(var(--depth) * 5%)); + } + --btn-shadow: 0 3px 2px -2px var(--btn-bg), + 0 4px 3px -2px var(--btn-bg); + @supports (color: color-mix(in lab, red, red)) { + --btn-shadow: 0 3px 2px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000), + 0 4px 3px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000); + } + --btn-noise: var(--fx-noise); + @media (hover: hover) { + &:hover { + --btn-bg: var(--btn-color, var(--color-base-200)); + @supports (color: color-mix(in lab, red, red)) { + --btn-bg: color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 7%); + } + } + } + &:focus-visible, &:has(:focus-visible) { + outline-width: 2px; + outline-style: solid; + isolation: isolate; + } + &:active:not(.btn-active) { + translate: 0 0.5px; + --btn-bg: var(--btn-color, var(--color-base-200)); + @supports (color: color-mix(in lab, red, red)) { + --btn-bg: color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 5%); + } + --btn-border: var(--btn-color, var(--color-base-200)); + @supports (color: color-mix(in lab, red, red)) { + --btn-border: color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 7%); + } + --btn-shadow: 0 0 0 0 oklch(0% 0 0/0), 0 0 0 0 oklch(0% 0 0/0); + } + &:is(input[type="checkbox"], input[type="radio"]) { + appearance: none; + &[aria-label]::after { + --tw-content: attr(aria-label); + content: var(--tw-content); + } + } + &:where(input:checked:not(.filter .btn)) { + --btn-color: var(--color-primary); + --btn-fg: var(--color-primary-content); + isolation: isolate; + } + } + &:disabled { + @layer daisyui.l1.l2 { + &:not(.btn-link, .btn-ghost) { + background-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-base-content) 10%, transparent); + } + box-shadow: none; + } + pointer-events: none; + --btn-border: #0000; + --btn-noise: none; + --btn-fg: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --btn-fg: color-mix(in oklch, var(--color-base-content) 20%, #0000); + } + } + } + &[disabled] { + @layer daisyui.l1.l2 { + &:not(.btn-link, .btn-ghost) { + background-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-base-content) 10%, transparent); + } + box-shadow: none; + } + pointer-events: none; + --btn-border: #0000; + --btn-noise: none; + --btn-fg: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --btn-fg: color-mix(in oklch, var(--color-base-content) 20%, #0000); + } + } + } + } + .loading { + @layer daisyui.l1.l2.l3 { + pointer-events: none; + display: inline-block; + aspect-ratio: 1 / 1; + background-color: currentcolor; + vertical-align: middle; + width: calc(var(--size-selector, 0.25rem) * 6); + mask-size: 100%; + mask-repeat: no-repeat; + mask-position: center; + mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E"); + } + } + .collapse { + &:not(td, tr, colgroup) { + visibility: revert-layer; + } + @layer daisyui.l1.l2.l3 { + display: grid; + position: relative; + overflow: hidden; + border-radius: var(--radius-box, 1rem); + width: 100%; + grid-template-rows: max-content 0fr; + grid-template-columns: minmax(0, 1fr); + isolation: isolate; + @media (prefers-reduced-motion: no-preference) { + transition: grid-template-rows 0.2s; + } + > input:is([type="checkbox"], [type="radio"]) { + grid-column-start: 1; + grid-row-start: 1; + appearance: none; + opacity: 0; + z-index: 1; + width: 100%; + padding: 1rem; + padding-inline-end: 3rem; + min-height: 1lh; + transition: background-color 0.2s ease-out; + } + &:is( [open], [tabindex]:focus:not(.collapse-close), [tabindex]:focus-within:not(.collapse-close) ), &:not(.collapse-close):has(> input:is([type="checkbox"], [type="radio"]):checked) { + grid-template-rows: max-content 1fr; + } + &:is( [open], [tabindex]:focus:not(.collapse-close), [tabindex]:focus-within:not(.collapse-close) ) > .collapse-content, &:not(.collapse-close) > :where(input:is([type="checkbox"], [type="radio"]):checked ~ .collapse-content) { + content-visibility: visible; + min-height: fit-content; + @supports not (content-visibility: visible) { + visibility: visible; + } + } + &:focus-visible, &:has(> input:is([type="checkbox"], [type="radio"]):focus-visible), &:has(summary:focus-visible) { + outline-color: var(--color-base-content); + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + } + &:not(.collapse-close) { + > input[type="checkbox"], > input[type="radio"]:not(:checked), > .collapse-title { + cursor: pointer; + } + } + &[tabindex]:focus:not(.collapse-close, .collapse[open]), &[tabindex]:focus-within:not(.collapse-close, .collapse[open]) { + > .collapse-title { + cursor: unset; + } + } + &:is( [open], [tabindex]:focus:not(.collapse-close), [tabindex]:focus-within:not(.collapse-close) ) > :where(.collapse-content), &:not(.collapse-close) > :where(input:is([type="checkbox"], [type="radio"]):checked ~ .collapse-content) { + padding-bottom: 1rem; + } + } + @layer daisyui.l1.l2 { + &:is([open]) { + &.collapse-arrow { + > .collapse-title:after { + @media (prefers-reduced-motion: no-preference) { + transform: translateY(-50%) rotate(225deg); + } + } + } + } + &.collapse-open { + &.collapse-arrow { + > .collapse-title:after { + @media (prefers-reduced-motion: no-preference) { + transform: translateY(-50%) rotate(225deg); + } + } + } + &.collapse-plus { + > .collapse-title:after { + --tw-content: "−"; + content: var(--tw-content); + } + } + } + &[tabindex].collapse-arrow:focus:not(.collapse-close), &.collapse-arrow[tabindex]:focus-within:not(.collapse-close) { + > .collapse-title:after { + transform: translateY(-50%) rotate(225deg); + } + } + &.collapse-arrow:not(.collapse-close) { + > input:is([type="checkbox"], [type="radio"]):checked ~ .collapse-title:after { + transform: translateY(-50%) rotate(225deg); + } + } + &[open] { + &.collapse-plus { + > .collapse-title:after { + --tw-content: "−"; + content: var(--tw-content); + } + } + } + &[tabindex].collapse-plus:focus:not(.collapse-close) { + > .collapse-title:after { + --tw-content: "−"; + content: var(--tw-content); + } + } + &.collapse-plus:not(.collapse-close) { + > input:is([type="checkbox"], [type="radio"]):checked ~ .collapse-title:after { + --tw-content: "−"; + content: var(--tw-content); + } + } + } + &:is(details) { + @layer daisyui.l1.l2.l3 { + width: 100%; + @media (prefers-reduced-motion: no-preference) { + &::details-content { + transition: content-visibility 0.2s allow-discrete, visibility 0.2s allow-discrete, padding 0.2s ease-out, background-color 0.2s ease-out, height 0.2s; + height: 0; + interpolate-size: allow-keywords; + } + &:where([open])::details-content { + height: auto; + } + } + & summary { + position: relative; + display: block; + &::-webkit-details-marker { + display: none; + } + } + & > .collapse-content { + content-visibility: visible; + } + } + } + &:is(details) summary { + @layer daisyui.l1.l2.l3 { + outline: none; + } + } + } + .collapse-content { + @layer daisyui.l1.l2.l3 { + grid-column-start: 1; + grid-row-start: 1; + } + @layer daisyui.l1.l2.l3 { + content-visibility: hidden; + grid-column-start: 1; + grid-row-start: 2; + min-height: 0; + padding-left: 1rem; + padding-right: 1rem; + cursor: unset; + @supports not (content-visibility: hidden) { + visibility: hidden; + } + @media (prefers-reduced-motion: no-preference) { + transition: content-visibility 0.2s allow-discrete, visibility 0.2s allow-discrete, padding 0.2s ease-out, background-color 0.2s ease-out; + } + } + } + .collapse { + visibility: collapse; + } + .visible { + visibility: visible; + } + .list { + @layer daisyui.l1.l2.l3 { + display: flex; + flex-direction: column; + font-size: 0.875rem; + .list-row { + --list-grid-cols: minmax(0, auto) 1fr; + position: relative; + display: grid; + grid-auto-flow: column; + gap: calc(0.25rem * 4); + border-radius: var(--radius-box); + padding: calc(0.25rem * 4); + word-break: break-word; + grid-template-columns: var(--list-grid-cols); + } + & > :not(:last-child) { + &.list-row, .list-row { + &:after { + content: ""; + border-bottom: var(--border) solid; + inset-inline: var(--radius-box); + position: absolute; + bottom: calc(0.25rem * 0); + border-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-base-content) 5%, transparent); + } + } + } + } + } + @layer daisyui.l1.l2 { + .list-row { + &:has(.list-col-grow:nth-child(1)) { + --list-grid-cols: 1fr; + } + &:has(.list-col-grow:nth-child(2)) { + --list-grid-cols: minmax(0, auto) 1fr; + } + &:has(.list-col-grow:nth-child(3)) { + --list-grid-cols: minmax(0, auto) minmax(0, auto) 1fr; + } + &:has(.list-col-grow:nth-child(4)) { + --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr; + } + &:has(.list-col-grow:nth-child(5)) { + --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr; + } + &:has(.list-col-grow:nth-child(6)) { + --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) + minmax(0, auto) 1fr; + } + > :not(.list-col-wrap) { + grid-row-start: 1; + } + } + } + } + .toggle { + @layer daisyui.l1.l2.l3 { + border: var(--border) solid currentColor; + color: var(--input-color); + position: relative; + display: inline-grid; + flex-shrink: 0; + cursor: pointer; + appearance: none; + place-content: center; + vertical-align: middle; + webkit-user-select: none; + user-select: none; + grid-template-columns: 0fr 1fr 1fr; + --radius-selector-max: calc( + var(--radius-selector) + var(--radius-selector) + var(--radius-selector) + ); + border-radius: calc( var(--radius-selector) + min(var(--toggle-p), var(--radius-selector-max)) + min(var(--border), var(--radius-selector-max)) ); + padding: var(--toggle-p); + box-shadow: 0 1px currentColor inset; + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000) inset; + } + transition: color 0.3s, grid-template-columns 0.2s; + --input-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --input-color: color-mix(in oklab, var(--color-base-content) 50%, #0000); + } + --toggle-p: calc(var(--size) * 0.125); + --size: calc(var(--size-selector, 0.25rem) * 6); + width: calc((var(--size) * 2) - (var(--border) + var(--toggle-p)) * 2); + height: var(--size); + > * { + z-index: 1; + grid-column: span 1 / span 1; + grid-column-start: 2; + grid-row-start: 1; + height: 100%; + cursor: pointer; + appearance: none; + background-color: transparent; + padding: calc(0.25rem * 0.5); + transition: opacity 0.2s, rotate 0.4s; + border: none; + &:focus { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + &:nth-child(2) { + color: var(--color-base-100); + rotate: 0deg; + } + &:nth-child(3) { + color: var(--color-base-100); + opacity: 0%; + rotate: -15deg; + } + } + &:has(:checked) { + > :nth-child(2) { + opacity: 0%; + rotate: 15deg; + } + > :nth-child(3) { + opacity: 100%; + rotate: 0deg; + } + } + &:before { + position: relative; + inset-inline-start: calc(0.25rem * 0); + grid-column-start: 2; + grid-row-start: 1; + aspect-ratio: 1 / 1; + height: 100%; + border-radius: var(--radius-selector); + background-color: currentcolor; + translate: 0; + --tw-content: ""; + content: var(--tw-content); + transition: background-color 0.1s, translate 0.2s, inset-inline-start 0.2s; + box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px currentColor; + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000); + } + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + } + @media (forced-colors: active) { + &:before { + outline-style: var(--tw-outline-style); + outline-width: 1px; + outline-offset: calc(1px * -1); + } + } + @media print { + &:before { + outline: 0.25rem solid; + outline-offset: -1rem; + } + } + &:focus-visible, &:has(:focus-visible) { + outline: 2px solid currentColor; + outline-offset: 2px; + } + &:checked, &[aria-checked="true"], &:has(> input:checked) { + grid-template-columns: 1fr 1fr 0fr; + background-color: var(--color-base-100); + --input-color: var(--color-base-content); + &:before { + background-color: currentcolor; + } + @starting-style { + &:before { + opacity: 0; + } + } + } + &:indeterminate { + grid-template-columns: 0.5fr 1fr 0.5fr; + } + &:disabled { + cursor: not-allowed; + opacity: 30%; + &:before { + background-color: transparent; + border: var(--border) solid currentColor; + } + } + } + } + .input { + @layer daisyui.l1.l2.l3 { + cursor: text; + border: var(--border) solid #0000; + position: relative; + display: inline-flex; + flex-shrink: 1; + appearance: none; + align-items: center; + gap: calc(0.25rem * 2); + background-color: var(--color-base-100); + padding-inline: calc(0.25rem * 3); + vertical-align: middle; + white-space: nowrap; + width: clamp(3rem, 20rem, 100%); + height: var(--size); + font-size: max(var(--font-size, 0.875rem), 0.875rem); + touch-action: manipulation; + border-start-start-radius: var(--join-ss, var(--radius-field)); + border-start-end-radius: var(--join-se, var(--radius-field)); + border-end-start-radius: var(--join-es, var(--radius-field)); + border-end-end-radius: var(--join-ee, var(--radius-field)); + border-color: var(--input-color); + box-shadow: 0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + } + --size: calc(var(--size-field, 0.25rem) * 10); + --input-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --input-color: color-mix(in oklab, var(--color-base-content) 20%, #0000); + } + &:where(input) { + display: inline-flex; + } + :where(input) { + display: inline-flex; + height: 100%; + width: 100%; + appearance: none; + background-color: transparent; + border: none; + &:focus, &:focus-within { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + } + :where(input[type="url"]), :where(input[type="email"]) { + direction: ltr; + } + :where(input[type="date"]) { + display: inline-flex; + } + &:focus, &:focus-within { + --input-color: var(--color-base-content); + box-shadow: 0 1px var(--input-color); + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000); + } + outline: 2px solid var(--input-color); + outline-offset: 2px; + isolation: isolate; + z-index: 1; + } + @media (pointer: coarse) { + @supports (-webkit-touch-callout: none) { + &:focus, &:focus-within { + --font-size: 1rem; + } + } + } + &:has(> input[disabled]), &:is(:disabled, [disabled]), fieldset:disabled & { + cursor: not-allowed; + border-color: var(--color-base-200); + background-color: var(--color-base-200); + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 40%, transparent); + } + &::placeholder { + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 20%, transparent); + } + } + box-shadow: none; + } + &:has(> input[disabled]) > input[disabled] { + cursor: not-allowed; + } + &::-webkit-date-and-time-value { + text-align: inherit; + } + &[type="number"] { + &::-webkit-inner-spin-button { + margin-block: calc(0.25rem * -3); + margin-inline-end: calc(0.25rem * -3); + } + } + &::-webkit-calendar-picker-indicator { + position: absolute; + inset-inline-end: 0.75em; + } + &:has(> input[type="date"]) { + :where(input[type="date"]) { + display: inline-flex; + webkit-appearance: none; + appearance: none; + } + input[type="date"]::-webkit-calendar-picker-indicator { + position: absolute; + inset-inline-end: 0.75em; + width: 1em; + height: 1em; + cursor: pointer; + } + } + } + } + .table { + @layer daisyui.l1.l2.l3 { + font-size: 0.875rem; + position: relative; + width: 100%; + border-collapse: separate; + --tw-border-spacing-x: calc(0.25rem * 0); + --tw-border-spacing-y: calc(0.25rem * 0); + border-spacing: var(--tw-border-spacing-x) var(--tw-border-spacing-y); + border-radius: var(--radius-box); + text-align: left; + &:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *) { + text-align: right; + } + tr.row-hover { + &, &:nth-child(even) { + &:hover { + @media (hover: hover) { + background-color: var(--color-base-200); + } + } + } + } + :where(th, td) { + padding-inline: calc(0.25rem * 4); + padding-block: calc(0.25rem * 3); + vertical-align: middle; + } + :where(thead, tfoot) { + white-space: nowrap; + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 60%, transparent); + } + font-size: 0.875rem; + font-weight: 600; + } + :where(tfoot tr:first-child :is(td, th)) { + border-top: var(--border) solid var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + border-top: var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000); + } + } + :where(.table-pin-rows thead tr) { + position: sticky; + top: calc(0.25rem * 0); + z-index: 1; + background-color: var(--color-base-100); + } + :where(.table-pin-rows tfoot tr) { + position: sticky; + bottom: calc(0.25rem * 0); + z-index: 1; + background-color: var(--color-base-100); + } + :where(.table-pin-cols tr th) { + position: sticky; + right: calc(0.25rem * 0); + left: calc(0.25rem * 0); + background-color: var(--color-base-100); + } + :where(thead tr :is(td, th), tbody tr:not(:last-child) :is(td, th)) { + border-bottom: var(--border) solid var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + border-bottom: var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000); + } + } + } + } + .avatar-offline { + @layer daisyui.l1.l2 { + &:before { + content: ""; + position: absolute; + z-index: 1; + display: block; + border-radius: calc(infinity * 1px); + background-color: var(--color-base-300); + outline: 2px solid var(--color-base-100); + width: 15%; + height: 15%; + top: 7%; + right: 7%; + } + } + } + .avatar-online { + @layer daisyui.l1.l2 { + &:before { + content: ""; + position: absolute; + z-index: 1; + display: block; + border-radius: calc(infinity * 1px); + background-color: var(--color-success); + outline: 2px solid var(--color-base-100); + width: 15%; + height: 15%; + top: 7%; + right: 7%; + } + } + } + .range { + @layer daisyui.l1.l2.l3 { + appearance: none; + webkit-appearance: none; + --range-thumb: var(--color-base-100); + --range-thumb-size: calc(var(--size-selector, 0.25rem) * 6); + --range-progress: currentColor; + --range-fill: 1; + --range-p: 0.25rem; + --range-bg: currentColor; + @supports (color: color-mix(in lab, red, red)) { + --range-bg: color-mix(in oklab, currentColor 10%, #0000); + } + cursor: pointer; + overflow: hidden; + background-color: transparent; + vertical-align: middle; + width: clamp(3rem, 20rem, 100%); + --radius-selector-max: calc( + var(--radius-selector) + var(--radius-selector) + var(--radius-selector) + ); + border-radius: calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max))); + border: none; + height: var(--range-thumb-size); + [dir="rtl"] & { + --range-dir: -1; + } + &:focus { + outline: none; + } + &:focus-visible { + outline: 2px solid; + outline-offset: 2px; + } + &::-webkit-slider-runnable-track { + width: 100%; + background-color: var(--range-bg); + border-radius: var(--radius-selector); + height: calc(var(--range-thumb-size) * 0.5); + } + @media (forced-colors: active) { + &::-webkit-slider-runnable-track { + border: 1px solid; + } + } + @media (forced-colors: active) { + &::-moz-range-track { + border: 1px solid; + } + } + &::-webkit-slider-thumb { + position: relative; + box-sizing: border-box; + border-radius: calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max))); + background-color: var(--range-thumb); + height: var(--range-thumb-size); + width: var(--range-thumb-size); + border: var(--range-p) solid; + appearance: none; + webkit-appearance: none; + top: 50%; + color: var(--range-progress); + transform: translateY(-50%); + box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px currentColor, 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir, 1) * -100rem) - (var(--range-dir, 1) * var(--range-thumb-size) / 2)) 0 0 calc(100rem * var(--range-fill)); + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir, 1) * -100rem) - (var(--range-dir, 1) * var(--range-thumb-size) / 2)) 0 0 calc(100rem * var(--range-fill)); + } + } + &::-moz-range-track { + width: 100%; + background-color: var(--range-bg); + border-radius: var(--radius-selector); + height: calc(var(--range-thumb-size) * 0.5); + } + &::-moz-range-thumb { + position: relative; + box-sizing: border-box; + border-radius: calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max))); + background-color: currentColor; + height: var(--range-thumb-size); + width: var(--range-thumb-size); + border: var(--range-p) solid; + top: 50%; + color: var(--range-progress); + box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px currentColor, 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir, 1) * -100rem) - (var(--range-dir, 1) * var(--range-thumb-size) / 2)) 0 0 calc(100rem * var(--range-fill)); + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir, 1) * -100rem) - (var(--range-dir, 1) * var(--range-thumb-size) / 2)) 0 0 calc(100rem * var(--range-fill)); + } + } + &:disabled { + cursor: not-allowed; + opacity: 30%; + } + } + } + .tabs-border { + @layer daisyui.l1.l2 { + > .tab { + --tab-border-color: #0000 #0000 var(--tab-border-color) #0000; + position: relative; + border-radius: var(--radius-field); + &:before { + content: ""; + background-color: var(--tab-border-color); + transition: background-color 0.2s ease; + width: 80%; + height: 3px; + border-radius: var(--radius-field); + bottom: 0; + left: 10%; + position: absolute; + } + &:is(.tab-active, [aria-selected="true"], [aria-current="true"], [aria-current="page"]):not( .tab-disabled, [disabled] ), &:is(input:checked), &:is(label:has(:checked)) { + &:before { + --tab-border-color: currentColor; + border-top: 3px solid; + } + } + } + } + } + .select { + @layer daisyui.l1.l2.l3 { + border: var(--border) solid #0000; + position: relative; + display: inline-flex; + flex-shrink: 1; + appearance: none; + align-items: center; + gap: calc(0.25rem * 1.5); + background-color: var(--color-base-100); + padding-inline-start: calc(0.25rem * 3); + padding-inline-end: calc(0.25rem * 7); + vertical-align: middle; + width: clamp(3rem, 20rem, 100%); + height: var(--size); + font-size: 0.875rem; + touch-action: manipulation; + border-start-start-radius: var(--join-ss, var(--radius-field)); + border-start-end-radius: var(--join-se, var(--radius-field)); + border-end-start-radius: var(--join-es, var(--radius-field)); + border-end-end-radius: var(--join-ee, var(--radius-field)); + background-image: linear-gradient(45deg, #0000 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, #0000 50%); + background-position: calc(100% - 20px) calc(1px + 50%), calc(100% - 16.1px) calc(1px + 50%); + background-size: 4px 4px, 4px 4px; + background-repeat: no-repeat; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + box-shadow: 0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + } + border-color: var(--input-color); + --input-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --input-color: color-mix(in oklab, var(--color-base-content) 20%, #0000); + } + --size: calc(var(--size-field, 0.25rem) * 10); + [dir="rtl"] & { + background-position: calc(0% + 12px) calc(1px + 50%), calc(0% + 16px) calc(1px + 50%); + &::picker(select), select::picker(select) { + translate: 0.5rem 0; + } + } + &[multiple] { + height: auto; + overflow: auto; + padding-block: calc(0.25rem * 3); + padding-inline-end: calc(0.25rem * 3); + background-image: none; + } + select { + margin-inline-start: calc(0.25rem * -3); + margin-inline-end: calc(0.25rem * -7); + width: calc(100% + 2.75rem); + appearance: none; + padding-inline-start: calc(0.25rem * 3); + padding-inline-end: calc(0.25rem * 7); + height: calc(100% - calc(var(--border) * 2)); + align-items: center; + background: inherit; + border-radius: inherit; + border-style: none; + &:focus, &:focus-within { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + &:not(:last-child) { + margin-inline-end: calc(0.25rem * -5.5); + background-image: none; + } + } + &:focus, &:focus-within { + --input-color: var(--color-base-content); + box-shadow: 0 1px var(--input-color); + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000); + } + outline: 2px solid var(--input-color); + outline-offset: 2px; + isolation: isolate; + z-index: 1; + } + &:has(> select[disabled]), &:is(:disabled, [disabled]), fieldset:disabled & { + cursor: not-allowed; + border-color: var(--color-base-200); + background-color: var(--color-base-200); + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 40%, transparent); + } + &::placeholder { + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 20%, transparent); + } + } + } + &:has(> select[disabled]) > select[disabled] { + cursor: not-allowed; + } + &, & select { + @supports (appearance: base-select) { + appearance: base-select; + } + @supports (appearance: base-select) { + &::picker(select) { + appearance: base-select; + } + } + &::picker(select) { + color: inherit; + max-height: min(24rem, 70dvh); + margin-inline: 0.5rem; + translate: -0.5rem 0; + border: var(--border) solid var(--color-base-200); + margin-block: calc(0.25rem * 2); + border-radius: var(--radius-box); + padding: calc(0.25rem * 2); + background-color: inherit; + box-shadow: 0 2px calc(var(--depth) * 3px) -2px oklch(0% 0 0/0.2); + box-shadow: 0 20px 25px -5px rgb(0 0 0 / calc(var(--depth) * 0.1)), 0 8px 10px -6px rgb(0 0 0 / calc(var(--depth) * 0.1)); + } + &::picker-icon { + display: none; + } + optgroup { + padding-top: 0.5em; + option { + &:nth-child(1) { + margin-top: 0.5em; + } + } + } + option { + border-radius: var(--radius-field); + padding-inline: calc(0.25rem * 3); + padding-block: calc(0.25rem * 1.5); + transition-property: color, background-color; + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + white-space: normal; + &:not(:disabled) { + &:hover, &:focus-visible { + cursor: pointer; + background-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-base-content) 10%, transparent); + } + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + &:active { + background-color: var(--color-neutral); + color: var(--color-neutral-content); + box-shadow: 0 2px calc(var(--depth) * 3px) -2px var(--color-neutral); + } + } + } + } + } + } + .swap { + @layer daisyui.l1.l2.l3 { + position: relative; + display: inline-grid; + cursor: pointer; + place-content: center; + vertical-align: middle; + webkit-user-select: none; + user-select: none; + input { + appearance: none; + border: none; + } + > * { + grid-column-start: 1; + grid-row-start: 1; + @media (prefers-reduced-motion: no-preference) { + transition-property: transform, rotate, opacity; + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + } + } + .swap-on, .swap-indeterminate, input:indeterminate ~ .swap-on { + opacity: 0%; + } + input:is(:checked, :indeterminate) { + & ~ .swap-off { + opacity: 0%; + } + } + input:checked ~ .swap-on, input:indeterminate ~ .swap-indeterminate { + opacity: 100%; + backface-visibility: visible; + } + } + } + .collapse-title { + @layer daisyui.l1.l2.l3 { + grid-column-start: 1; + grid-row-start: 1; + } + @layer daisyui.l1.l2.l3 { + position: relative; + width: 100%; + padding: 1rem; + padding-inline-end: 3rem; + min-height: 1lh; + transition: background-color 0.2s ease-out; + } + } + .menu-horizontal { + @layer daisyui.l1.l2 { + display: inline-flex; + flex-direction: row; + & > li:not(.menu-title) > details { + & > ul { + position: absolute; + margin-inline-start: calc(0.25rem * 0); + margin-top: calc(0.25rem * 4); + transform-origin: top; + border-radius: var(--radius-box); + background-color: var(--color-base-100); + padding-block: calc(0.25rem * 2); + padding-inline-end: calc(0.25rem * 2); + opacity: 0%; + scale: 95%; + box-shadow: 0 1px 3px 0 oklch(0% 0 0/0.1), 0 1px 2px -1px oklch(0% 0 0/0.1); + @media (prefers-reduced-motion: no-preference) { + @starting-style { + scale: 95%; + opacity: 0; + } + animation: menu 0.2s; + transition-property: opacity, scale, display; + transition-behavior: allow-discrete; + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + } + } + &[open] > ul { + opacity: 100%; + scale: 100%; + } + } + & > li > details > ul { + &:before { + --tw-content: none; + content: var(--tw-content); + } + } + } + } + .avatar { + @layer daisyui.l1.l2.l3 { + position: relative; + display: inline-flex; + vertical-align: middle; + & > div { + display: block; + aspect-ratio: 1 / 1; + overflow: hidden; + } + img { + height: 100%; + width: 100%; + object-fit: cover; + } + } + } + .checkbox { + @layer daisyui.l1.l2.l3 { + border: var(--border) solid var(--input-color, var(--color-base-content)); + @supports (color: color-mix(in lab, red, red)) { + border: var(--border) solid var(--input-color, color-mix(in oklab, var(--color-base-content) 20%, #0000)); + } + position: relative; + display: inline-block; + flex-shrink: 0; + cursor: pointer; + appearance: none; + border-radius: var(--radius-selector); + padding: calc(0.25rem * 1); + vertical-align: middle; + color: var(--color-base-content); + box-shadow: 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 0 #0000 inset, 0 0 #0000; + transition: background-color 0.2s, box-shadow 0.2s; + --size: calc(var(--size-selector, 0.25rem) * 6); + width: var(--size); + height: var(--size); + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + &:before { + --tw-content: ""; + content: var(--tw-content); + display: block; + width: 100%; + height: 100%; + rotate: 45deg; + background-color: currentcolor; + opacity: 0%; + transition: clip-path 0.3s, opacity 0.1s, rotate 0.3s, translate 0.3s; + transition-delay: 0.1s; + clip-path: polygon(20% 100%, 20% 80%, 50% 80%, 50% 80%, 70% 80%, 70% 100%); + box-shadow: 0px 3px 0 0px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + font-size: 1rem; + line-height: 0.75; + } + &:focus-visible { + outline: 2px solid var(--input-color, currentColor); + outline-offset: 2px; + } + &:checked, &[aria-checked="true"] { + background-color: var(--input-color, #0000); + box-shadow: 0 0 #0000 inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1)); + &:before { + clip-path: polygon(20% 100%, 20% 80%, 50% 80%, 50% 0%, 70% 0%, 70% 100%); + opacity: 100%; + } + @media (forced-colors: active) { + &:before { + rotate: 0deg; + background-color: transparent; + --tw-content: "✔︎"; + clip-path: none; + } + } + @media print { + &:before { + rotate: 0deg; + background-color: transparent; + --tw-content: "✔︎"; + clip-path: none; + } + } + } + &:indeterminate { + background-color: var( --input-color, var(--color-base-content) ); + @supports (color: color-mix(in lab, red, red)) { + background-color: var( --input-color, color-mix(in oklab, var(--color-base-content) 20%, #0000) ); + } + &:before { + rotate: 0deg; + opacity: 100%; + translate: 0 -35%; + clip-path: polygon(20% 100%, 20% 80%, 50% 80%, 50% 80%, 80% 80%, 80% 100%); + } + } + } + &:disabled { + @layer daisyui.l1.l2 { + cursor: not-allowed; + opacity: 20%; + } + } + } + .radio { + @layer daisyui.l1.l2.l3 { + position: relative; + display: inline-block; + flex-shrink: 0; + cursor: pointer; + appearance: none; + border-radius: calc(infinity * 1px); + padding: calc(0.25rem * 1); + vertical-align: middle; + border: var(--border) solid var(--input-color, currentColor); + @supports (color: color-mix(in lab, red, red)) { + border: var(--border) solid var(--input-color, color-mix(in srgb, currentColor 20%, #0000)); + } + box-shadow: 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset; + --size: calc(var(--size-selector, 0.25rem) * 6); + width: var(--size); + height: var(--size); + color: var(--input-color, currentColor); + &:before { + display: block; + width: 100%; + height: 100%; + border-radius: calc(infinity * 1px); + --tw-content: ""; + content: var(--tw-content); + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + } + &:focus-visible { + outline: 2px solid currentColor; + } + &:checked, &[aria-checked="true"] { + border-color: currentcolor; + background-color: var(--color-base-100); + @media (prefers-reduced-motion: no-preference) { + animation: radio 0.2s ease-out; + } + &:before { + background-color: currentcolor; + box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1)); + } + @media (forced-colors: active) { + &:before { + outline-style: var(--tw-outline-style); + outline-width: 1px; + outline-offset: calc(1px * -1); + } + } + @media print { + &:before { + outline: 0.25rem solid; + outline-offset: -1rem; + } + } + } + } + &:disabled { + @layer daisyui.l1.l2 { + cursor: not-allowed; + opacity: 20%; + } + } + } + .rating { + @layer daisyui.l1.l2.l3 { + position: relative; + display: inline-flex; + vertical-align: middle; + & input { + border: none; + appearance: none; + } + :where(*) { + height: calc(0.25rem * 6); + width: calc(0.25rem * 6); + border-radius: 0; + background-color: var(--color-base-content); + opacity: 20%; + @media (prefers-reduced-motion: no-preference) { + animation: rating 0.25s ease-out; + } + &:is(input) { + cursor: pointer; + } + } + & .rating-hidden { + width: calc(0.25rem * 2); + background-color: transparent; + } + input[type="radio"]:checked { + background-image: none; + } + * { + &:checked, &[aria-checked="true"], &[aria-current="true"], &:has(~ *:checked, ~ *[aria-checked="true"], ~ *[aria-current="true"]) { + opacity: 100%; + } + &:focus-visible { + scale: 1.1; + @media (prefers-reduced-motion: no-preference) { + transition: scale 0.2s ease-out; + } + } + } + & *:active:focus { + animation: none; + scale: 1.1; + } + } + @layer daisyui.l1.l2 { + &.rating-xs :where(*:not(.rating-hidden)) { + width: calc(0.25rem * 4); + height: calc(0.25rem * 4); + } + &.rating-sm :where(*:not(.rating-hidden)) { + width: calc(0.25rem * 5); + height: calc(0.25rem * 5); + } + &.rating-md :where(*:not(.rating-hidden)) { + width: calc(0.25rem * 6); + height: calc(0.25rem * 6); + } + &.rating-lg :where(*:not(.rating-hidden)) { + width: calc(0.25rem * 7); + height: calc(0.25rem * 7); + } + &.rating-xl :where(*:not(.rating-hidden)) { + width: calc(0.25rem * 8); + height: calc(0.25rem * 8); + } + } + } + .drawer { + @layer daisyui.l1.l2.l3 { + position: relative; + display: grid; + width: 100%; + grid-auto-columns: max-content auto; + } + } + .card { + @layer daisyui.l1.l2.l3 { + position: relative; + display: flex; + flex-direction: column; + border-radius: var(--radius-box); + outline-width: 2px; + transition: outline 0.2s ease-in-out; + outline: 0 solid #0000; + outline-offset: 2px; + &:focus { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + &:focus-visible { + outline-color: currentColor; + } + :where(figure:first-child) { + overflow: hidden; + border-start-start-radius: inherit; + border-start-end-radius: inherit; + border-end-start-radius: unset; + border-end-end-radius: unset; + } + :where(figure:last-child) { + overflow: hidden; + border-start-start-radius: unset; + border-start-end-radius: unset; + border-end-start-radius: inherit; + border-end-end-radius: inherit; + } + figure { + display: flex; + align-items: center; + justify-content: center; + } + &:has(> input:is(input[type="checkbox"], input[type="radio"])) { + cursor: pointer; + user-select: none; + } + &:has(> :checked) { + outline: 2px solid currentColor; + } + } + } + .progress { + @layer daisyui.l1.l2.l3 { + position: relative; + height: calc(0.25rem * 2); + width: 100%; + appearance: none; + overflow: hidden; + border-radius: var(--radius-box); + background-color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, currentcolor 20%, transparent); + } + color: var(--color-base-content); + &:indeterminate { + background-image: repeating-linear-gradient( 90deg, currentColor -1%, currentColor 10%, #0000 10%, #0000 90% ); + background-size: 200%; + background-position-x: 15%; + @media (prefers-reduced-motion: no-preference) { + animation: progress 5s ease-in-out infinite; + } + @supports (-moz-appearance: none) { + &::-moz-progress-bar { + background-color: transparent; + @media (prefers-reduced-motion: no-preference) { + animation: progress 5s ease-in-out infinite; + background-image: repeating-linear-gradient( 90deg, currentColor -1%, currentColor 10%, #0000 10%, #0000 90% ); + background-size: 200%; + background-position-x: 15%; + } + } + } + } + @supports (-moz-appearance: none) { + &::-moz-progress-bar { + border-radius: var(--radius-box); + background-color: currentcolor; + } + } + @supports (-webkit-appearance: none) { + &::-webkit-progress-bar { + border-radius: var(--radius-box); + background-color: transparent; + } + &::-webkit-progress-value { + border-radius: var(--radius-box); + background-color: currentColor; + } + } + } + } + .absolute { + position: absolute; + } + .fixed { + position: fixed; + } + .relative { + position: relative; + } + .static { + position: static; + } + .start-2 { + inset-inline-start: calc(var(--spacing) * 2); + } + .dropdown-center { + @layer daisyui.l1.l2 { + --anchor-h: center; + :where(.dropdown-content) { + inset-inline-end: calc(1/2 * 100%); + translate: 50% 0; + [dir="rtl"] & { + translate: -50% 0; + } + } + &.dropdown-left { + --anchor-h: left; + --anchor-v: center; + .dropdown-content { + top: auto; + bottom: calc(1/2 * 100%); + translate: 0 50%; + } + } + &.dropdown-right { + --anchor-h: right; + --anchor-v: center; + .dropdown-content { + top: auto; + bottom: calc(1/2 * 100%); + translate: 0 50%; + } + } + } + } + .dropdown-end { + @layer daisyui.l1.l2 { + --anchor-h: span-left; + :where(.dropdown-content) { + inset-inline-end: calc(0.25rem * 0); + translate: 0 0; + [dir="rtl"] & { + translate: 0 0; + } + } + &.dropdown-left { + --anchor-h: left; + --anchor-v: span-top; + .dropdown-content { + top: auto; + bottom: calc(0.25rem * 0); + } + } + &.dropdown-right { + --anchor-h: right; + --anchor-v: span-top; + .dropdown-content { + top: auto; + bottom: calc(0.25rem * 0); + } + } + } + } + .end-0 { + inset-inline-end: calc(var(--spacing) * 0); + } + .end-1 { + inset-inline-end: calc(var(--spacing) * 1); + } + .end-4 { + inset-inline-end: calc(var(--spacing) * 4); + } + .dropdown-bottom { + @layer daisyui.l1.l2 { + --anchor-v: bottom; + .dropdown-content { + top: 100%; + bottom: auto; + transform-origin: top; + } + } + } + .top-1 { + top: calc(var(--spacing) * 1); + } + .top-2 { + top: calc(var(--spacing) * 2); + } + .top-4 { + top: calc(var(--spacing) * 4); + } + .top-\[100\%\] { + top: 100%; + } + .right-2 { + right: calc(var(--spacing) * 2); + } + .right-\[10px\] { + right: 10px; + } + .bottom-0 { + bottom: calc(var(--spacing) * 0); + } + .left-0 { + left: calc(var(--spacing) * 0); + } + .left-\[-10px\] { + left: -10px; + } + .left-\[-30px\] { + left: -30px; + } + .left-\[0\] { + left: 0; + } + .left-\[10px\] { + left: 10px; + } + .file-input { + @layer daisyui.l1.l2.l3 { + cursor: pointer; + cursor: pointer; + border: var(--border) solid #0000; + display: inline-flex; + appearance: none; + align-items: center; + background-color: var(--color-base-100); + vertical-align: middle; + webkit-user-select: none; + user-select: none; + width: clamp(3rem, 20rem, 100%); + height: var(--size); + padding-inline-end: 0.75rem; + font-size: 0.875rem; + line-height: 2; + border-start-start-radius: var(--join-ss, var(--radius-field)); + border-start-end-radius: var(--join-se, var(--radius-field)); + border-end-start-radius: var(--join-es, var(--radius-field)); + border-end-end-radius: var(--join-ee, var(--radius-field)); + border-color: var(--input-color); + box-shadow: 0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + } + --size: calc(var(--size-field, 0.25rem) * 10); + --input-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --input-color: color-mix(in oklab, var(--color-base-content) 20%, #0000); + } + &::file-selector-button { + margin-inline-end: calc(0.25rem * 4); + cursor: pointer; + padding-inline: calc(0.25rem * 4); + webkit-user-select: none; + user-select: none; + height: calc(100% + var(--border) * 2); + margin-block: calc(var(--border) * -1); + margin-inline-start: calc(var(--border) * -1); + font-size: 0.875rem; + color: var(--btn-fg); + border-width: var(--border); + border-style: solid; + border-color: var(--btn-border); + border-start-start-radius: calc(var(--join-ss, var(--radius-field) - var(--border))); + border-end-start-radius: calc(var(--join-es, var(--radius-field) - var(--border))); + font-weight: 600; + background-color: var(--btn-bg); + background-size: calc(var(--noise) * 100%); + background-image: var(--btn-noise); + text-shadow: 0 0.5px oklch(1 0 0 / calc(var(--depth) * 0.15)); + box-shadow: 0 0.5px 0 0.5px white inset, var(--btn-shadow); + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 0.5px 0 0.5px color-mix( in oklab, color-mix(in oklab, white 30%, var(--btn-bg)) calc(var(--depth) * 20%), #0000 ) inset, var(--btn-shadow); + } + --size: calc(var(--size-field, 0.25rem) * 10); + --btn-bg: var(--btn-color, var(--color-base-200)); + --btn-fg: var(--color-base-content); + --btn-border: var(--btn-bg); + @supports (color: color-mix(in lab, red, red)) { + --btn-border: color-mix(in oklab, var(--btn-bg), #000 5%); + } + --btn-shadow: 0 3px 2px -2px var(--btn-bg), + 0 4px 3px -2px var(--btn-bg); + @supports (color: color-mix(in lab, red, red)) { + --btn-shadow: 0 3px 2px -2px color-mix(in oklab, var(--btn-bg) 30%, #0000), + 0 4px 3px -2px color-mix(in oklab, var(--btn-bg) 30%, #0000); + } + --btn-noise: var(--fx-noise); + } + &:focus { + --input-color: var(--color-base-content); + box-shadow: 0 1px var(--input-color); + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, var(--input-color) 10%, #0000); + } + outline: 2px solid var(--input-color); + outline-offset: 2px; + isolation: isolate; + } + &:has(> input[disabled]), &:is(:disabled, [disabled]) { + cursor: not-allowed; + border-color: var(--color-base-200); + background-color: var(--color-base-200); + &::placeholder { + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 20%, transparent); + } + } + box-shadow: none; + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklch, var(--color-base-content) 20%, #0000); + } + &::file-selector-button { + cursor: not-allowed; + border-color: var(--color-base-200); + background-color: var(--color-base-200); + --btn-border: #0000; + --btn-noise: none; + --btn-fg: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --btn-fg: color-mix(in oklch, var(--color-base-content) 20%, #0000); + } + } + } + } + } + .textarea { + @layer daisyui.l1.l2.l3 { + border: var(--border) solid #0000; + min-height: calc(0.25rem * 20); + flex-shrink: 1; + appearance: none; + border-radius: var(--radius-field); + background-color: var(--color-base-100); + padding-block: calc(0.25rem * 2); + vertical-align: middle; + width: clamp(3rem, 20rem, 100%); + padding-inline-start: 0.75rem; + padding-inline-end: 0.75rem; + font-size: max(var(--font-size, 0.875rem), 0.875rem); + touch-action: manipulation; + border-color: var(--input-color); + box-shadow: 0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset; + } + --input-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --input-color: color-mix(in oklab, var(--color-base-content) 20%, #0000); + } + textarea { + appearance: none; + background-color: transparent; + border: none; + &:focus, &:focus-within { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + } + &:focus, &:focus-within { + --input-color: var(--color-base-content); + box-shadow: 0 1px var(--input-color); + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000); + } + outline: 2px solid var(--input-color); + outline-offset: 2px; + isolation: isolate; + } + @media (pointer: coarse) { + @supports (-webkit-touch-callout: none) { + &:focus, &:focus-within { + --font-size: 1rem; + } + } + } + &:has(> textarea[disabled]), &:is(:disabled, [disabled]) { + cursor: not-allowed; + border-color: var(--color-base-200); + background-color: var(--color-base-200); + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 40%, transparent); + } + &::placeholder { + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 20%, transparent); + } + } + box-shadow: none; + } + &:has(> textarea[disabled]) > textarea[disabled] { + cursor: not-allowed; + } + } + } + .btn-active { + @layer daisyui.l1.l2 { + --btn-bg: var(--btn-color, var(--color-base-200)); + @supports (color: color-mix(in lab, red, red)) { + --btn-bg: color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 7%); + } + --btn-shadow: 0 0 0 0 oklch(0% 0 0/0), 0 0 0 0 oklch(0% 0 0/0); + isolation: isolate; + } + } + .isolate { + isolation: isolate; + } + .modal-backdrop { + @layer daisyui.l1.l2.l3 { + grid-column-start: 1; + grid-row-start: 1; + display: grid; + align-self: stretch; + justify-self: stretch; + color: transparent; + z-index: -1; + button { + cursor: pointer; + } + } + } + .z-\[1\] { + z-index: 1; + } + .order-\[1\] { + order: 1; + } + .col-span-2 { + grid-column: span 2 / span 2; + } + .col-span-3 { + grid-column: span 3 / span 3; + } + .col-span-4 { + grid-column: span 4 / span 4; + } + .col-span-5 { + grid-column: span 5 / span 5; + } + .col-span-8 { + grid-column: span 8 / span 8; + } + .modal-box { + @layer daisyui.l1.l2.l3 { + grid-column-start: 1; + grid-row-start: 1; + max-height: 100vh; + width: calc(11/12 * 100%); + max-width: 32rem; + background-color: var(--color-base-100); + padding: calc(0.25rem * 6); + transition: translate 0.3s ease-out, scale 0.3s ease-out, opacity 0.2s ease-out 0.05s, box-shadow 0.3s ease-out; + border-top-left-radius: var(--modal-tl, var(--radius-box)); + border-top-right-radius: var(--modal-tr, var(--radius-box)); + border-bottom-left-radius: var(--modal-bl, var(--radius-box)); + border-bottom-right-radius: var(--modal-br, var(--radius-box)); + scale: 95%; + opacity: 0; + box-shadow: oklch(0% 0 0/ 0.25) 0px 25px 50px -12px; + overflow-y: auto; + overscroll-behavior: contain; + } + } + .drawer-content { + @layer daisyui.l1.l2.l3 { + grid-column-start: 2; + grid-row-start: 1; + min-width: calc(0.25rem * 0); + } + } + .drawer-end { + @layer daisyui.l1.l2 { + grid-auto-columns: auto max-content; + > .drawer-toggle { + & ~ .drawer-content { + grid-column-start: 1; + } + & ~ .drawer-side { + grid-column-start: 2; + justify-items: end; + } + & ~ .drawer-side > *:not(.drawer-overlay) { + translate: 100%; + [dir="rtl"] & { + translate: -100%; + } + } + &:checked ~ .drawer-side > *:not(.drawer-overlay) { + translate: 0%; + } + } + } + } + .container { + width: 100%; + @media (width >= 40rem) { + max-width: 40rem; + } + @media (width >= 48rem) { + max-width: 48rem; + } + @media (width >= 64rem) { + max-width: 64rem; + } + @media (width >= 80rem) { + max-width: 80rem; + } + @media (width >= 96rem) { + max-width: 96rem; + } + } + .m-4 { + margin: calc(var(--spacing) * 4); + } + .m-\[0_0_0_10px\] { + margin: 0 0 0 10px; + } + .m-\[0_0_6px_0\] { + margin: 0 0 6px 0; + } + .m-\[0_0_7px_0\] { + margin: 0 0 7px 0; + } + .m-\[0_0_12px\] { + margin: 0 0 12px; + } + .m-\[0_0_13px\] { + margin: 0 0 13px; + } + .m-\[0_0_16px\] { + margin: 0 0 16px; + } + .m-\[0_0_16px_0\] { + margin: 0 0 16px 0; + } + .m-\[0_0_16px_16px\] { + margin: 0 0 16px 16px; + } + .m-\[0_0_20px_0\] { + margin: 0 0 20px 0; + } + .m-\[0_4px_0_0\] { + margin: 0 4px 0 0; + } + .m-\[0_5px_0_0\] { + margin: 0 5px 0 0; + } + .m-\[0_6px_6px_0\] { + margin: 0 6px 6px 0; + } + .m-\[0_7px_0px_0\] { + margin: 0 7px 0px 0; + } + .m-\[0_10px\] { + margin: 0 10px; + } + .m-\[0_10px_0_0\] { + margin: 0 10px 0 0; + } + .m-\[0_10px_10px_0\] { + margin: 0 10px 10px 0; + } + .m-\[0_14px_0_7px\] { + margin: 0 14px 0 7px; + } + .m-\[0_16px_0_0\] { + margin: 0 16px 0 0; + } + .m-\[0_16px_16px_0\] { + margin: 0 16px 16px 0; + } + .m-\[0_17px_7px_0\] { + margin: 0 17px 7px 0; + } + .m-\[0_20px_0_10px\] { + margin: 0 20px 0 10px; + } + .m-\[0_35px_0_10px\] { + margin: 0 35px 0 10px; + } + .m-\[0_76px\] { + margin: 0 76px; + } + .m-\[0_auto_16px_auto\] { + margin: 0 auto 16px auto; + } + .m-\[0_auto_20px\] { + margin: 0 auto 20px; + } + .m-\[5px_0\] { + margin: 5px 0; + } + .m-\[6px_0\] { + margin: 6px 0; + } + .m-\[7px_0\] { + margin: 7px 0; + } + .m-\[10px_0\] { + margin: 10px 0; + } + .m-\[10px_0_0\] { + margin: 10px 0 0; + } + .m-\[10px_0_15px\] { + margin: 10px 0 15px; + } + .m-\[10px_10px_0_0\] { + margin: 10px 10px 0 0; + } + .m-\[12px_0\] { + margin: 12px 0; + } + .m-\[12px_0_16px\] { + margin: 12px 0 16px; + } + .m-\[12px_0_20px\] { + margin: 12px 0 20px; + } + .m-\[13pX_0\] { + margin: 13pX 0; + } + .m-\[13px_0\] { + margin: 13px 0; + } + .m-\[13px_0_20px\] { + margin: 13px 0 20px; + } + .m-\[15px\] { + margin: 15px; + } + .m-\[15px_0\] { + margin: 15px 0; + } + .m-\[15px_0_22px\] { + margin: 15px 0 22px; + } + .m-\[16px\] { + margin: 16px; + } + .m-\[16px_-16px\] { + margin: 16px -16px; + } + .m-\[16px_0\] { + margin: 16px 0; + } + .m-\[16px_0_16px_16px\] { + margin: 16px 0 16px 16px; + } + .m-\[17px_auto_20px\] { + margin: 17px auto 20px; + } + .m-\[20px_-16px\] { + margin: 20px -16px; + } + .m-\[20px_0\!important\] { + margin: 20px 0!important; + } + .m-\[20px_0\] { + margin: 20px 0; + } + .m-\[20px_0_16px\] { + margin: 20px 0 16px; + } + .m-\[20px_0_25px\] { + margin: 20px 0 25px; + } + .m-\[auto\] { + margin: auto; + } + .m-\[auto_0_auto_auto\] { + margin: auto 0 auto auto; + } + .m-auto { + margin: auto; + } + .filter { + @layer daisyui.l1.l2.l3 { + display: flex; + flex-wrap: wrap; + input[type="radio"] { + width: auto; + } + input { + overflow: hidden; + opacity: 100%; + scale: 1; + transition: margin 0.1s, opacity 0.3s, padding 0.3s, border-width 0.1s; + &:not(:last-child) { + margin-inline-end: calc(0.25rem * 1); + } + &.filter-reset { + aspect-ratio: 1 / 1; + &::after { + --tw-content: "×"; + content: var(--tw-content); + } + } + } + &:not(:has(input:checked:not(.filter-reset))) { + .filter-reset, input[type="reset"] { + scale: 0; + border-width: 0; + margin-inline: calc(0.25rem * 0); + width: calc(0.25rem * 0); + padding-inline: calc(0.25rem * 0); + opacity: 0%; + } + } + &:has(input:checked:not(.filter-reset)) { + input:not(:checked, .filter-reset, input[type="reset"]) { + scale: 0; + border-width: 0; + margin-inline: calc(0.25rem * 0); + width: calc(0.25rem * 0); + padding-inline: calc(0.25rem * 0); + opacity: 0%; + } + } + } + } + .mx-2 { + margin-inline: calc(var(--spacing) * 2); + } + .mx-\[5px\] { + margin-inline: 5px; + } + .mx-\[6px\] { + margin-inline: 6px; + } + .mx-\[7px\] { + margin-inline: 7px; + } + .mx-\[10px\] { + margin-inline: 10px; + } + .mx-auto { + margin-inline: auto; + } + .input-md { + @layer daisyui.l1.l2 { + --size: calc(var(--size-field, 0.25rem) * 10); + font-size: max(var(--font-size, 0.875rem), 0.875rem); + &[type="number"] { + &::-webkit-inner-spin-button { + margin-block: calc(0.25rem * -3); + margin-inline-end: calc(0.25rem * -3); + } + } + } + } + .input-sm { + @layer daisyui.l1.l2 { + --size: calc(var(--size-field, 0.25rem) * 8); + font-size: max(var(--font-size, 0.75rem), 0.75rem); + &[type="number"] { + &::-webkit-inner-spin-button { + margin-block: calc(0.25rem * -2); + margin-inline-end: calc(0.25rem * -3); + } + } + } + } + .my-0 { + margin-block: calc(var(--spacing) * 0); + } + .my-1 { + margin-block: calc(var(--spacing) * 1); + } + .my-\[5px\] { + margin-block: 5px; + } + .my-\[10px\] { + margin-block: 10px; + } + .my-\[12px\] { + margin-block: 12px; + } + .my-\[15px\] { + margin-block: 15px; + } + .my-\[20px\] { + margin-block: 20px; + } + .my-\[25px\] { + margin-block: 25px; + } + .label { + @layer daisyui.l1.l2.l3 { + display: inline-flex; + align-items: center; + gap: calc(0.25rem * 1.5); + white-space: nowrap; + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 60%, transparent); + } + &:has(input) { + cursor: pointer; + } + &:is(.input > *, .select > *) { + display: flex; + height: calc(100% - 0.5rem); + align-items: center; + padding-inline: calc(0.25rem * 3); + white-space: nowrap; + font-size: inherit; + &:first-child { + margin-inline-start: calc(0.25rem * -3); + margin-inline-end: calc(0.25rem * 3); + border-inline-end: var(--border) solid currentColor; + @supports (color: color-mix(in lab, red, red)) { + border-inline-end: var(--border) solid color-mix(in oklab, currentColor 10%, #0000); + } + } + &:last-child { + margin-inline-start: calc(0.25rem * 3); + margin-inline-end: calc(0.25rem * -3); + border-inline-start: var(--border) solid currentColor; + @supports (color: color-mix(in lab, red, red)) { + border-inline-start: var(--border) solid color-mix(in oklab, currentColor 10%, #0000); + } + } + } + } + } + .join-item { + &:where(*:not(:first-child, :disabled, [disabled], .btn-disabled)) { + margin-inline-start: calc(var(--border, 1px) * -1); + margin-block-start: 0; + } + &:where(*:is(:disabled, [disabled], .btn-disabled)) { + border-width: var(--border, 1px) 0 var(--border, 1px) var(--border, 1px); + } + } + .-ms-2 { + margin-inline-start: calc(var(--spacing) * -2); + } + .ms-1 { + margin-inline-start: calc(var(--spacing) * 1); + } + .ms-1\.5 { + margin-inline-start: calc(var(--spacing) * 1.5); + } + .ms-auto { + margin-inline-start: auto; + } + .me-1 { + margin-inline-end: calc(var(--spacing) * 1); + } + .me-1\.5 { + margin-inline-end: calc(var(--spacing) * 1.5); + } + .tabs-box { + @layer daisyui.l1.l2 { + background-color: var(--color-base-200); + padding: calc(0.25rem * 1); + --tabs-box-radius: calc(3 * var(--radius-field)); + border-radius: calc( min(var(--tab-height) / 2, var(--radius-field)) + min(0.25rem, var(--tabs-box-radius)) ); + box-shadow: 0 -0.5px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 0.5px oklch(0% 0 0 / calc(var(--depth) * 0.05)) inset; + > .tab { + border-radius: var(--radius-field); + border-style: none; + &:focus-visible, &:is(label:has(:checked:focus-visible)) { + outline-offset: 2px; + } + } + > :is(.tab-active, [aria-selected="true"], [aria-current="true"], [aria-current="page"]):not( .tab-disabled, [disabled] ), > :is(input:checked), > :is(label:has(:checked)) { + background-color: var(--tab-bg, var(--color-base-100)); + box-shadow: 0 1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px 1px -1px var(--color-neutral), 0 1px 6px -4px var(--color-neutral); + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px 1px -1px color-mix(in oklab, var(--color-neutral) calc(var(--depth) * 50%), #0000), 0 1px 6px -4px color-mix(in oklab, var(--color-neutral) calc(var(--depth) * 100%), #0000); + } + @media (forced-colors: active) { + border: 1px solid; + } + } + > .tab-content { + margin-top: calc(0.25rem * 1); + height: calc(100% - var(--tab-height) + var(--border) - 0.5rem); + border-radius: calc( min(var(--tab-height) / 2, var(--radius-field)) + min(0.25rem, var(--tabs-box-radius)) - var(--border) ); + } + } + } + .-mt-25 { + margin-top: calc(var(--spacing) * -25); + } + .mt-0 { + margin-top: calc(var(--spacing) * 0); + } + .mt-0\.5 { + margin-top: calc(var(--spacing) * 0.5); + } + .mt-1 { + margin-top: calc(var(--spacing) * 1); + } + .mt-2 { + margin-top: calc(var(--spacing) * 2); + } + .mt-3 { + margin-top: calc(var(--spacing) * 3); + } + .mt-4 { + margin-top: calc(var(--spacing) * 4); + } + .mt-5 { + margin-top: calc(var(--spacing) * 5); + } + .mt-\[3px\] { + margin-top: 3px; + } + .mt-\[5px\] { + margin-top: 5px; + } + .mt-\[6px\] { + margin-top: 6px; + } + .mt-\[7px\] { + margin-top: 7px; + } + .mt-\[8px\] { + margin-top: 8px; + } + .mt-\[10px\] { + margin-top: 10px; + } + .mt-\[12px\] { + margin-top: 12px; + } + .mt-\[13px\] { + margin-top: 13px; + } + .mt-\[15px\] { + margin-top: 15px; + } + .mt-\[17px\] { + margin-top: 17px; + } + .mt-\[20px\] { + margin-top: 20px; + } + .mt-\[25px\] { + margin-top: 25px; + } + .mt-\[30px\] { + margin-top: 30px; + } + .mt-auto { + margin-top: auto; + } + .breadcrumbs { + @layer daisyui.l1.l2.l3 { + max-width: 100%; + overflow-x: auto; + padding-block: calc(0.25rem * 2); + > menu, > ul, > ol { + display: flex; + min-height: min-content; + align-items: center; + white-space: nowrap; + > li { + display: flex; + align-items: center; + > * { + display: flex; + cursor: pointer; + align-items: center; + gap: calc(0.25rem * 2); + &:hover { + @media (hover: hover) { + text-decoration-line: underline; + } + } + &:focus { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + &:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; + } + } + & + *:before { + content: ""; + margin-right: calc(0.25rem * 3); + margin-left: calc(0.25rem * 2); + display: block; + height: calc(0.25rem * 1.5); + width: calc(0.25rem * 1.5); + opacity: 40%; + rotate: 45deg; + border-top: 1px solid; + border-right: 1px solid; + background-color: #0000; + } + [dir="rtl"] & + *:before { + rotate: -135deg; + } + } + } + } + } + .mr-1 { + margin-right: calc(var(--spacing) * 1); + } + .mr-2 { + margin-right: calc(var(--spacing) * 2); + } + .mr-3 { + margin-right: calc(var(--spacing) * 3); + } + .mr-4 { + margin-right: calc(var(--spacing) * 4); + } + .mr-\[3px\] { + margin-right: 3px; + } + .mr-\[5px\] { + margin-right: 5px; + } + .mr-\[6px\] { + margin-right: 6px; + } + .mr-\[7\.5px\] { + margin-right: 7.5px; + } + .mr-\[7px\] { + margin-right: 7px; + } + .mr-\[8px\] { + margin-right: 8px; + } + .mr-\[10px\] { + margin-right: 10px; + } + .mr-\[12px\] { + margin-right: 12px; + } + .mr-\[13px\] { + margin-right: 13px; + } + .mr-\[15px\] { + margin-right: 15px; + } + .mr-\[16px\] { + margin-right: 16px; + } + .mr-\[20px\] { + margin-right: 20px; + } + .mr-\[25px\] { + margin-right: 25px; + } + .mr-\[34px\] { + margin-right: 34px; + } + .mr-\[40px\] { + margin-right: 40px; + } + .mr-\[45px\] { + margin-right: 45px; + } + .-mb-px { + margin-bottom: -1px; + } + .mb-1 { + margin-bottom: calc(var(--spacing) * 1); + } + .mb-2 { + margin-bottom: calc(var(--spacing) * 2); + } + .mb-3 { + margin-bottom: calc(var(--spacing) * 3); + } + .mb-4 { + margin-bottom: calc(var(--spacing) * 4); + } + .mb-5 { + margin-bottom: calc(var(--spacing) * 5); + } + .mb-6 { + margin-bottom: calc(var(--spacing) * 6); + } + .mb-\[2px\] { + margin-bottom: 2px; + } + .mb-\[3px\] { + margin-bottom: 3px; + } + .mb-\[5px\] { + margin-bottom: 5px; + } + .mb-\[6px\] { + margin-bottom: 6px; + } + .mb-\[7px\] { + margin-bottom: 7px; + } + .mb-\[8px\] { + margin-bottom: 8px; + } + .mb-\[10px\] { + margin-bottom: 10px; + } + .mb-\[12px\] { + margin-bottom: 12px; + } + .mb-\[13px\] { + margin-bottom: 13px; + } + .mb-\[14px\] { + margin-bottom: 14px; + } + .mb-\[15px\] { + margin-bottom: 15px; + } + .mb-\[16px\] { + margin-bottom: 16px; + } + .mb-\[17px\] { + margin-bottom: 17px; + } + .mb-\[18px\] { + margin-bottom: 18px; + } + .mb-\[20px\] { + margin-bottom: 20px; + } + .mb-\[23px\] { + margin-bottom: 23px; + } + .mb-\[24px\] { + margin-bottom: 24px; + } + .mb-\[25px\] { + margin-bottom: 25px; + } + .mb-\[30px\] { + margin-bottom: 30px; + } + .mb-\[50px\] { + margin-bottom: 50px; + } + .mb-\[100px\] { + margin-bottom: 100px; + } + .ml-1 { + margin-left: calc(var(--spacing) * 1); + } + .ml-2 { + margin-left: calc(var(--spacing) * 2); + } + .ml-4 { + margin-left: calc(var(--spacing) * 4); + } + .ml-\[2px\] { + margin-left: 2px; + } + .ml-\[3px\] { + margin-left: 3px; + } + .ml-\[5px\] { + margin-left: 5px; + } + .ml-\[6px\] { + margin-left: 6px; + } + .ml-\[7px\] { + margin-left: 7px; + } + .ml-\[10px\] { + margin-left: 10px; + } + .ml-\[12px\] { + margin-left: 12px; + } + .ml-\[15px\] { + margin-left: 15px; + } + .ml-\[16px\] { + margin-left: 16px; + } + .ml-\[25px\] { + margin-left: 25px; + } + .status { + @layer daisyui.l1.l2.l3 { + display: inline-block; + aspect-ratio: 1 / 1; + width: calc(0.25rem * 2); + height: calc(0.25rem * 2); + border-radius: var(--radius-selector); + background-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-base-content) 20%, transparent); + } + background-position: center; + background-repeat: no-repeat; + vertical-align: middle; + color: color-mix(in srgb, #000 30%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-black) 30%, transparent); + } + background-image: radial-gradient( circle at 35% 30%, oklch(1 0 0 / calc(var(--depth) * 0.5)), #0000 ); + box-shadow: 0 2px 3px -1px currentColor; + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 2px 3px -1px color-mix(in oklab, currentColor calc(var(--depth) * 100%), #0000); + } + } + } + .badge { + @layer daisyui.l1.l2.l3 { + display: inline-flex; + align-items: center; + justify-content: center; + gap: calc(0.25rem * 2); + border-radius: var(--radius-selector); + vertical-align: middle; + color: var(--badge-fg); + border: var(--border) solid var(--badge-color, var(--color-base-200)); + font-size: 0.875rem; + width: fit-content; + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + background-color: var(--badge-bg); + --badge-bg: var(--badge-color, var(--color-base-100)); + --badge-fg: var(--color-base-content); + --size: calc(var(--size-selector, 0.25rem) * 6); + height: var(--size); + padding-inline: calc(var(--size) / 2 - var(--border)); + } + } + .tabs { + @layer daisyui.l1.l2.l3 { + display: flex; + flex-wrap: wrap; + --tabs-height: auto; + --tabs-direction: row; + --tab-height: calc(var(--size-field, 0.25rem) * 10); + height: var(--tabs-height); + flex-direction: var(--tabs-direction); + } + } + .footer { + @layer daisyui.l1.l2.l3 { + display: grid; + width: 100%; + grid-auto-flow: row; + place-items: start; + column-gap: calc(0.25rem * 4); + row-gap: calc(0.25rem * 10); + font-size: 0.875rem; + line-height: 1.25rem; + & > * { + display: grid; + place-items: start; + gap: calc(0.25rem * 2); + } + &.footer-center { + grid-auto-flow: column dense; + place-items: center; + text-align: center; + & > * { + place-items: center; + } + } + } + } + .card-body { + @layer daisyui.l1.l2.l3 { + display: flex; + flex: auto; + flex-direction: column; + gap: calc(0.25rem * 2); + padding: var(--card-p, 1.5rem); + font-size: var(--card-fs, 0.875rem); + :where(p) { + flex-grow: 1; + } + } + } + .fieldset { + @layer daisyui.l1.l2.l3 { + display: grid; + gap: calc(0.25rem * 1.5); + padding-block: calc(0.25rem * 1); + font-size: 0.75rem; + grid-template-columns: 1fr; + grid-auto-rows: max-content; + } + } + .chat { + @layer daisyui.l1.l2.l3 { + display: grid; + grid-auto-rows: min-content; + column-gap: calc(0.25rem * 3); + padding-block: calc(0.25rem * 1); + --mask-chat: url("data:image/svg+xml,%3csvg width='13' height='13' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='M0 11.5004C0 13.0004 2 13.0004 2 13.0004H12H13V0.00036329L12.5 0C12.5 0 11.977 2.09572 11.8581 2.50033C11.6075 3.35237 10.9149 4.22374 9 5.50036C6 7.50036 0 10.0004 0 11.5004Z'/%3e%3c/svg%3e"); + } + } + .join { + display: inline-flex; + align-items: stretch; + --join-ss: 0; + --join-se: 0; + --join-es: 0; + --join-ee: 0; + :where(.join-item) { + border-start-start-radius: var(--join-ss, 0); + border-start-end-radius: var(--join-se, 0); + border-end-start-radius: var(--join-es, 0); + border-end-end-radius: var(--join-ee, 0); + * { + --join-ss: var(--radius-field); + --join-se: var(--radius-field); + --join-es: var(--radius-field); + --join-ee: var(--radius-field); + } + } + > .join-item:where(:first-child) { + --join-ss: var(--radius-field); + --join-se: 0; + --join-es: var(--radius-field); + --join-ee: 0; + } + :first-child:not(:last-child) { + :where(.join-item) { + --join-ss: var(--radius-field); + --join-se: 0; + --join-es: var(--radius-field); + --join-ee: 0; + } + } + > .join-item:where(:last-child) { + --join-ss: 0; + --join-se: var(--radius-field); + --join-es: 0; + --join-ee: var(--radius-field); + } + :last-child:not(:first-child) { + :where(.join-item) { + --join-ss: 0; + --join-se: var(--radius-field); + --join-es: 0; + --join-ee: var(--radius-field); + } + } + > .join-item:where(:only-child) { + --join-ss: var(--radius-field); + --join-se: var(--radius-field); + --join-es: var(--radius-field); + --join-ee: var(--radius-field); + } + :only-child { + :where(.join-item) { + --join-ss: var(--radius-field); + --join-se: var(--radius-field); + --join-es: var(--radius-field); + --join-ee: var(--radius-field); + } + } + } + .line-clamp-1 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + } + .mask { + @layer daisyui.l1.l2.l3 { + display: inline-block; + vertical-align: middle; + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; + } + } + .block { + display: block; + } + .contents { + display: contents; + } + .flex { + display: flex; + } + .grid { + display: grid; + } + .hidden { + display: none; + } + .inline { + display: inline; + } + .inline-block { + display: inline-block; + } + .inline-flex { + display: inline-flex; + } + .table { + display: table; + } + .btn-circle { + @layer daisyui.l1.l2 { + border-radius: calc(infinity * 1px); + padding-inline: calc(0.25rem * 0); + width: var(--size); + height: var(--size); + } + } + .btn-square { + @layer daisyui.l1.l2 { + padding-inline: calc(0.25rem * 0); + width: var(--size); + height: var(--size); + } + } + .size-1\.5 { + width: calc(var(--spacing) * 1.5); + height: calc(var(--spacing) * 1.5); + } + .size-3 { + width: calc(var(--spacing) * 3); + height: calc(var(--spacing) * 3); + } + .size-3\.5 { + width: calc(var(--spacing) * 3.5); + height: calc(var(--spacing) * 3.5); + } + .size-4 { + width: calc(var(--spacing) * 4); + height: calc(var(--spacing) * 4); + } + .size-4\.5 { + width: calc(var(--spacing) * 4.5); + height: calc(var(--spacing) * 4.5); + } + .size-5 { + width: calc(var(--spacing) * 5); + height: calc(var(--spacing) * 5); + } + .size-9 { + width: calc(var(--spacing) * 9); + height: calc(var(--spacing) * 9); + } + .size-10 { + width: calc(var(--spacing) * 10); + height: calc(var(--spacing) * 10); + } + .size-12 { + width: calc(var(--spacing) * 12); + height: calc(var(--spacing) * 12); + } + .size-20 { + width: calc(var(--spacing) * 20); + height: calc(var(--spacing) * 20); + } + .size-full { + width: 100%; + height: 100%; + } + .status-sm { + @layer daisyui.l1.l2 { + width: calc(0.25rem * 1); + height: calc(0.25rem * 1); + } + } + .h-1 { + height: calc(var(--spacing) * 1); + } + .h-2\.5 { + height: calc(var(--spacing) * 2.5); + } + .h-5 { + height: calc(var(--spacing) * 5); + } + .h-5\.5 { + height: calc(var(--spacing) * 5.5); + } + .h-9 { + height: calc(var(--spacing) * 9); + } + .h-\[6px\] { + height: 6px; + } + .h-\[13px\] { + height: 13px; + } + .h-\[14\.5px\] { + height: 14.5px; + } + .h-\[14px\] { + height: 14px; + } + .h-\[15px\] { + height: 15px; + } + .h-\[16px\] { + height: 16px; + } + .h-\[18px\] { + height: 18px; + } + .h-\[20px\] { + height: 20px; + } + .h-\[25px\] { + height: 25px; + } + .h-\[26px\] { + height: 26px; + } + .h-\[28px\] { + height: 28px; + } + .h-\[30px\] { + height: 30px; + } + .h-\[32px\] { + height: 32px; + } + .h-\[33px\] { + height: 33px; + } + .h-\[34px\] { + height: 34px; + } + .h-\[35px\] { + height: 35px; + } + .h-\[36px\] { + height: 36px; + } + .h-\[38px\] { + height: 38px; + } + .h-\[40px\] { + height: 40px; + } + .h-\[50px\] { + height: 50px; + } + .h-\[70px\] { + height: 70px; + } + .h-\[75px\] { + height: 75px; + } + .h-\[80px\] { + height: 80px; + } + .h-\[100\%\] { + height: 100%; + } + .h-\[100px\] { + height: 100px; + } + .h-\[120px\] { + height: 120px; + } + .h-\[225px\] { + height: 225px; + } + .h-\[300px\] { + height: 300px; + } + .h-\[350px\] { + height: 350px; + } + .h-\[380px\] { + height: 380px; + } + .h-\[400px\] { + height: 400px; + } + .h-full { + height: 100%; + } + .h-px { + height: 1px; + } + .h-screen { + height: 100vh; + } + .max-h-400 { + max-height: calc(var(--spacing) * 400); + } + .max-h-\[80px\] { + max-height: 80px; + } + .max-h-\[100px\] { + max-height: 100px; + } + .max-h-\[120px\] { + max-height: 120px; + } + .max-h-\[150px\] { + max-height: 150px; + } + .max-h-\[170px\] { + max-height: 170px; + } + .max-h-\[250px\] { + max-height: 250px; + } + .max-h-\[300px\] { + max-height: 300px; + } + .max-h-\[350px\] { + max-height: 350px; + } + .max-h-\[400px\] { + max-height: 400px; + } + .max-h-\[500px\] { + max-height: 500px; + } + .min-h-\[30px\] { + min-height: 30px; + } + .min-h-\[32px\] { + min-height: 32px; + } + .min-h-\[33px\] { + min-height: 33px; + } + .min-h-\[34px\] { + min-height: 34px; + } + .min-h-\[35px\] { + min-height: 35px; + } + .min-h-\[36px\] { + min-height: 36px; + } + .min-h-\[38px\] { + min-height: 38px; + } + .min-h-\[40px\] { + min-height: 40px; + } + .min-h-\[70px\] { + min-height: 70px; + } + .min-h-\[76px\] { + min-height: 76px; + } + .min-h-\[90px\] { + min-height: 90px; + } + .min-h-\[100px\] { + min-height: 100px; + } + .min-h-\[122px\] { + min-height: 122px; + } + .loading-sm { + @layer daisyui.l1.l2 { + width: calc(var(--size-selector, 0.25rem) * 5); + } + } + .w-1\/3 { + width: calc(1/3 * 100%); + } + .w-2 { + width: calc(var(--spacing) * 2); + } + .w-2\.5 { + width: calc(var(--spacing) * 2.5); + } + .w-3 { + width: calc(var(--spacing) * 3); + } + .w-6 { + width: calc(var(--spacing) * 6); + } + .w-7xl { + width: var(--container-7xl); + } + .w-8 { + width: calc(var(--spacing) * 8); + } + .w-11\/12 { + width: calc(11/12 * 100%); + } + .w-12 { + width: calc(var(--spacing) * 12); + } + .w-24 { + width: calc(var(--spacing) * 24); + } + .w-30 { + width: calc(var(--spacing) * 30); + } + .w-32 { + width: calc(var(--spacing) * 32); + } + .w-34 { + width: calc(var(--spacing) * 34); + } + .w-40 { + width: calc(var(--spacing) * 40); + } + .w-48 { + width: calc(var(--spacing) * 48); + } + .w-64 { + width: calc(var(--spacing) * 64); + } + .w-72 { + width: calc(var(--spacing) * 72); + } + .w-80 { + width: calc(var(--spacing) * 80); + } + .w-84 { + width: calc(var(--spacing) * 84); + } + .w-96 { + width: calc(var(--spacing) * 96); + } + .w-100 { + width: calc(var(--spacing) * 100); + } + .w-\[10\%\] { + width: 10%; + } + .w-\[13px\] { + width: 13px; + } + .w-\[14\.5px\] { + width: 14.5px; + } + .w-\[14px\] { + width: 14px; + } + .w-\[15px\] { + width: 15px; + } + .w-\[16px\] { + width: 16px; + } + .w-\[18px\] { + width: 18px; + } + .w-\[20\%\] { + width: 20%; + } + .w-\[20px\] { + width: 20px; + } + .w-\[25\%\] { + width: 25%; + } + .w-\[26px\] { + width: 26px; + } + .w-\[30\%\] { + width: 30%; + } + .w-\[30px\] { + width: 30px; + } + .w-\[32\%\] { + width: 32%; + } + .w-\[32px\] { + width: 32px; + } + .w-\[35\%\] { + width: 35%; + } + .w-\[38px\] { + width: 38px; + } + .w-\[40\%\] { + width: 40%; + } + .w-\[40px\] { + width: 40px; + } + .w-\[45\%\] { + width: 45%; + } + .w-\[45px\] { + width: 45px; + } + .w-\[49\%\] { + width: 49%; + } + .w-\[50\%\] { + width: 50%; + } + .w-\[50px\] { + width: 50px; + } + .w-\[55\%\] { + width: 55%; + } + .w-\[60px\] { + width: 60px; + } + .w-\[65px\] { + width: 65px; + } + .w-\[70\%\] { + width: 70%; + } + .w-\[70px\] { + width: 70px; + } + .w-\[75px\] { + width: 75px; + } + .w-\[78\%\] { + width: 78%; + } + .w-\[80\%\] { + width: 80%; + } + .w-\[80px\] { + width: 80px; + } + .w-\[80px_\!important\] { + width: 80px !important; + } + .w-\[82px\] { + width: 82px; + } + .w-\[85px\] { + width: 85px; + } + .w-\[86px\] { + width: 86px; + } + .w-\[90\%\] { + width: 90%; + } + .w-\[90px\] { + width: 90px; + } + .w-\[95px\] { + width: 95px; + } + .w-\[97\%\] { + width: 97%; + } + .w-\[100\%\] { + width: 100%; + } + .w-\[100px\] { + width: 100px; + } + .w-\[105px\] { + width: 105px; + } + .w-\[106px\] { + width: 106px; + } + .w-\[113px\] { + width: 113px; + } + .w-\[115px\] { + width: 115px; + } + .w-\[120px\] { + width: 120px; + } + .w-\[120px_\!important\] { + width: 120px !important; + } + .w-\[125px\] { + width: 125px; + } + .w-\[130px\] { + width: 130px; + } + .w-\[135px\] { + width: 135px; + } + .w-\[135px_\!important\] { + width: 135px !important; + } + .w-\[140px\] { + width: 140px; + } + .w-\[150px\] { + width: 150px; + } + .w-\[155px\] { + width: 155px; + } + .w-\[160px\] { + width: 160px; + } + .w-\[165px_\!important\] { + width: 165px !important; + } + .w-\[170px\] { + width: 170px; + } + .w-\[175px\] { + width: 175px; + } + .w-\[180px\] { + width: 180px; + } + .w-\[185px\] { + width: 185px; + } + .w-\[190px\] { + width: 190px; + } + .w-\[200px\] { + width: 200px; + } + .w-\[205px\] { + width: 205px; + } + .w-\[210px\] { + width: 210px; + } + .w-\[215px\] { + width: 215px; + } + .w-\[220px\] { + width: 220px; + } + .w-\[230px\] { + width: 230px; + } + .w-\[240px\] { + width: 240px; + } + .w-\[245px\] { + width: 245px; + } + .w-\[250px\] { + width: 250px; + } + .w-\[258px\] { + width: 258px; + } + .w-\[260px\] { + width: 260px; + } + .w-\[265px\] { + width: 265px; + } + .w-\[270px\] { + width: 270px; + } + .w-\[275px\] { + width: 275px; + } + .w-\[280px\] { + width: 280px; + } + .w-\[300px\] { + width: 300px; + } + .w-\[300px_\!important\] { + width: 300px !important; + } + .w-\[312px\] { + width: 312px; + } + .w-\[345px\] { + width: 345px; + } + .w-\[375px\] { + width: 375px; + } + .w-\[400px\] { + width: 400px; + } + .w-\[470px\] { + width: 470px; + } + .w-\[540px\] { + width: 540px; + } + .w-\[650px\] { + width: 650px; + } + .w-\[700px\] { + width: 700px; + } + .w-\[850px\] { + width: 850px; + } + .w-\[852px\] { + width: 852px; + } + .w-\[860px\] { + width: 860px; + } + .w-\[1140px\] { + width: 1140px; + } + .w-\[auto_\!important\] { + width: auto !important; + } + .w-\[calc\(100\%-36px\)\] { + width: calc(100% - 36px); + } + .w-\[calc\(100\%\/3\)\] { + width: calc(100% / 3); + } + .w-\[calc\(100\%_-25\%_-16px\)\] { + width: calc(100% - 25% - 16px); + } + .w-\[calc\(100\%_-140px\)\] { + width: calc(100% - 140px); + } + .w-\[calc\(100\%_-160px\)\] { + width: calc(100% - 160px); + } + .w-\[fit-content\] { + width: fit-content; + } + .w-full { + width: 100%; + } + .max-w-3xl { + max-width: var(--container-3xl); + } + .max-w-5xl { + max-width: var(--container-5xl); + } + .max-w-\[52px\] { + max-width: 52px; + } + .max-w-\[600px\] { + max-width: 600px; + } + .max-w-\[700px\] { + max-width: 700px; + } + .max-w-\[1024px\] { + max-width: 1024px; + } + .max-w-\[1140px\] { + max-width: 1140px; + } + .max-w-\[1141px\] { + max-width: 1141px; + } + .max-w-full { + max-width: 100%; + } + .max-w-sm { + max-width: var(--container-sm); + } + .max-w-xl { + max-width: var(--container-xl); + } + .max-w-xs { + max-width: var(--container-xs); + } + .min-w-0 { + min-width: calc(var(--spacing) * 0); + } + .min-w-\[36px\] { + min-width: 36px; + } + .min-w-\[100px\] { + min-width: 100px; + } + .min-w-\[140px\] { + min-width: 140px; + } + .min-w-\[160px\] { + min-width: 160px; + } + .min-w-\[200px\] { + min-width: 200px; + } + .grow { + flex-grow: 1; + } + .translate-x-2 { + --tw-translate-x: calc(var(--spacing) * 2); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .-translate-y-4 { + --tw-translate-y: calc(var(--spacing) * -4); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .translate-y-4 { + --tw-translate-y: calc(var(--spacing) * 4); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .scale-\[1\.2\] { + scale: 1.2; + } + .scale-\[1\.4\] { + scale: 1.4; + } + .scale-\[1\.5\] { + scale: 1.5; + } + .transform { + transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); + } + .link { + @layer daisyui.l1.l2.l3 { + cursor: pointer; + text-decoration-line: underline; + &:focus { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + &:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; + } + } + } + .cursor-pointer { + cursor: pointer; + } + .resize { + resize: both; + } + .list-inside { + list-style-position: inside; + } + .list-disc { + list-style-type: disc; + } + .grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + .grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + .grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + .grid-cols-6 { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } + .grid-cols-12 { + grid-template-columns: repeat(12, minmax(0, 1fr)); + } + .flex-col { + flex-direction: column; + } + .flex-row { + flex-direction: row; + } + .flex-wrap { + flex-wrap: wrap; + } + .items-center { + align-items: center; + } + .items-end { + align-items: flex-end; + } + .items-start { + align-items: flex-start; + } + .justify-between { + justify-content: space-between; + } + .justify-center { + justify-content: center; + } + .justify-end { + justify-content: flex-end; + } + .justify-start { + justify-content: flex-start; + } + .gap-0\.5 { + gap: calc(var(--spacing) * 0.5); + } + .gap-1 { + gap: calc(var(--spacing) * 1); + } + .gap-1\.5 { + gap: calc(var(--spacing) * 1.5); + } + .gap-2 { + gap: calc(var(--spacing) * 2); + } + .gap-2\.5 { + gap: calc(var(--spacing) * 2.5); + } + .gap-3 { + gap: calc(var(--spacing) * 3); + } + .gap-5 { + gap: calc(var(--spacing) * 5); + } + .gap-\[10px\] { + gap: 10px; + } + .gap-\[15px\] { + gap: 15px; + } + .gap-\[30px\] { + gap: 30px; + } + .-space-y-0\.5 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * -0.5) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * -0.5) * calc(1 - var(--tw-space-y-reverse))); + } + } + .divide-base-300 { + :where(& > :not(:last-child)) { + border-color: var(--color-base-300); + } + } + .truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .overflow-auto { + overflow: auto; + } + .overflow-hidden { + overflow: hidden; + } + .overflow-x-auto { + overflow-x: auto; + } + .overflow-y-auto { + overflow-y: auto; + } + .overflow-y-scroll { + overflow-y: scroll; + } + .rounded { + border-radius: 0.25rem; + } + .rounded-\[0_0_10px_10px\] { + border-radius: 0 0 10px 10px; + } + .rounded-\[0_0_15px_15px\] { + border-radius: 0 0 15px 15px; + } + .rounded-\[4px\] { + border-radius: 4px; + } + .rounded-\[5px\] { + border-radius: 5px; + } + .rounded-\[9px\] { + border-radius: 9px; + } + .rounded-\[10px\] { + border-radius: 10px; + } + .rounded-\[10px_10px_0_0\] { + border-radius: 10px 10px 0 0; + } + .rounded-\[12px\] { + border-radius: 12px; + } + .rounded-\[15px\] { + border-radius: 15px; + } + .rounded-\[15px_15px_0_0\] { + border-radius: 15px 15px 0 0; + } + .rounded-\[20px\] { + border-radius: 20px; + } + .rounded-\[50\%\] { + border-radius: 50%; + } + .rounded-\[50px\] { + border-radius: 50px; + } + .rounded-box { + border-radius: var(--radius-box); + } + .rounded-box { + border-radius: var(--radius-box); + } + .rounded-full { + border-radius: calc(infinity * 1px); + } + .rounded-lg { + border-radius: var(--radius-lg); + } + .rounded-md { + border-radius: var(--radius-md); + } + .rounded-none { + border-radius: 0; + } + .rounded-sm { + border-radius: var(--radius-sm); + } + .rounded-t-box { + border-top-left-radius: var(--radius-box); + border-top-right-radius: var(--radius-box); + } + .rounded-t-box { + border-top-left-radius: var(--radius-box); + border-top-right-radius: var(--radius-box); + } + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } + .border-0 { + border-style: var(--tw-border-style); + border-width: 0px; + } + .border-\[1px\] { + border-style: var(--tw-border-style); + border-width: 1px; + } + .border-\[3px\] { + border-style: var(--tw-border-style); + border-width: 3px; + } + .border-\[4px\] { + border-style: var(--tw-border-style); + border-width: 4px; + } + .border-t { + border-top-style: var(--tw-border-style); + border-top-width: 1px; + } + .border-t-\[1px\] { + border-top-style: var(--tw-border-style); + border-top-width: 1px; + } + .border-t-\[2px\] { + border-top-style: var(--tw-border-style); + border-top-width: 2px; + } + .border-t-\[6px\] { + border-top-style: var(--tw-border-style); + border-top-width: 6px; + } + .border-b { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } + .border-b-\[1px\] { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } + .border-b-\[6px\] { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 6px; + } + .border-b-\[8px\] { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 8px; + } + .btn-dash { + @layer daisyui.l1 { + &:not( .btn-active, :hover, :active:focus, :focus-visible, input:checked:not(.filter .btn), :disabled, [disabled], .btn-disabled ) { + --btn-shadow: ""; + --btn-bg: #0000; + --btn-fg: var(--btn-color); + --btn-border: var(--btn-color); + --btn-noise: none; + } + @media (hover: none) { + &:not(.btn-active, :active, :focus-visible, input:checked:not(.filter .btn)):hover { + --btn-shadow: ""; + --btn-bg: #0000; + --btn-fg: var(--btn-color); + --btn-border: var(--btn-color); + --btn-noise: none; + } + } + } + border-style: dashed; + } + .border-dashed { + --tw-border-style: dashed; + border-style: dashed; + } + .border-none { + --tw-border-style: none; + border-style: none; + } + .badge-ghost { + @layer daisyui.l1.l2 { + border-color: var(--color-base-200); + background-color: var(--color-base-200); + color: var(--color-base-content); + background-image: none; + } + } + .badge-soft { + @layer daisyui.l1.l2 { + color: var(--badge-color, var(--color-base-content)); + background-color: var(--badge-color, var(--color-base-content)); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix( in oklab, var(--badge-color, var(--color-base-content)) 8%, var(--color-base-100) ); + } + border-color: var(--badge-color, var(--color-base-content)); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix( in oklab, var(--badge-color, var(--color-base-content)) 10%, var(--color-base-100) ); + } + background-image: none; + } + } + .border-\[\#000\] { + border-color: #000; + } + .border-\[\#6B7280\] { + border-color: #6B7280; + } + .border-\[\#9E9E9E\] { + border-color: #9E9E9E; + } + .border-\[\#0041E8\] { + border-color: #0041E8; + } + .border-\[\#0041E8_\!important\] { + border-color: #0041E8 !important; + } + .border-\[\#0041e8\] { + border-color: #0041e8; + } + .border-\[\#79B0E2\] { + border-color: #79B0E2; + } + .border-\[\#79b0e2\] { + border-color: #79b0e2; + } + .border-\[\#D8D8D8\] { + border-color: #D8D8D8; + } + .border-\[\#E00000\] { + border-color: #E00000; + } + .border-\[\#ECECEC\] { + border-color: #ECECEC; + } + .border-\[\#F6F6F6\] { + border-color: #F6F6F6; + } + .border-\[\#FAFAFB\] { + border-color: #FAFAFB; + } + .border-\[\#d8d8d8\] { + border-color: #d8d8d8; + } + .border-\[\#f6f6f6\] { + border-color: #f6f6f6; + } + .border-\[\#f8f8f8\] { + border-color: #f8f8f8; + } + .border-base-200 { + border-color: var(--color-base-200); + } + .border-base-300 { + border-color: var(--color-base-300); + } + .border-base-content\/5 { + border-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-base-content) 5%, transparent); + } + } + .border-blue-600 { + border-color: var(--color-blue-600); + } + .border-blue-700 { + border-color: var(--color-blue-700); + } + .border-gray-100 { + border-color: var(--color-gray-100); + } + .border-gray-200 { + border-color: var(--color-gray-200); + } + .border-gray-600 { + border-color: var(--color-gray-600); + } + .border-primary-content\/10 { + border-color: var(--color-primary-content); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-primary-content) 10%, transparent); + } + } + .border-b-\[\#FAFAFB\] { + border-bottom-color: #FAFAFB; + } + .status-error { + @layer daisyui.l1.l2 { + background-color: var(--color-error); + color: var(--color-error); + } + } + .status-primary { + @layer daisyui.l1.l2 { + background-color: var(--color-primary); + color: var(--color-primary); + } + } + .table-zebra { + @layer daisyui.l1.l2 { + tbody { + tr { + &:where(:nth-child(even)) { + background-color: var(--color-base-200); + :where(.table-pin-cols tr th) { + background-color: var(--color-base-200); + } + } + &.row-hover { + &, &:where(:nth-child(even)) { + &:hover { + @media (hover: hover) { + background-color: var(--color-base-300); + } + } + } + } + } + } + } + } + .bg-\[\#0041E8\] { + background-color: #0041E8; + } + .bg-\[\#E00000\] { + background-color: #E00000; + } + .bg-\[\#E5E5E5\] { + background-color: #E5E5E5; + } + .bg-\[\#E8F7F2\] { + background-color: #E8F7F2; + } + .bg-\[\#ECECEC\] { + background-color: #ECECEC; + } + .bg-\[\#ECECEC_\!important\] { + background-color: #ECECEC !important; + } + .bg-\[\#F0F8FF\] { + background-color: #F0F8FF; + } + .bg-\[\#F0F9F2\] { + background-color: #F0F9F2; + } + .bg-\[\#F1F1F1\] { + background-color: #F1F1F1; + } + .bg-\[\#F5F7FF\] { + background-color: #F5F7FF; + } + .bg-\[\#F6F6F6\] { + background-color: #F6F6F6; + } + .bg-\[\#F7F5FF\] { + background-color: #F7F5FF; + } + .bg-\[\#F9F2F2\] { + background-color: #F9F2F2; + } + .bg-\[\#F9F9F9\] { + background-color: #F9F9F9; + } + .bg-\[\#FAFAFB\] { + background-color: #FAFAFB; + } + .bg-\[\#FFC700\] { + background-color: #FFC700; + } + .bg-\[\#FFE6C1\] { + background-color: #FFE6C1; + } + .bg-\[\#FFEEEB\] { + background-color: #FFEEEB; + } + .bg-\[\#f5f5f5\] { + background-color: #f5f5f5; + } + .bg-\[\#f6f6f6\] { + background-color: #f6f6f6; + } + .bg-\[\#f7f7ff\] { + background-color: #f7f7ff; + } + .bg-\[\#fff\] { + background-color: #fff; + } + .bg-\[rgba\(0\,0\,0\,0\.60\)\] { + background-color: rgba(0,0,0,0.60); + } + .bg-base-100 { + background-color: var(--color-base-100); + } + .bg-base-200 { + background-color: var(--color-base-200); + } + .bg-base-200\/30 { + background-color: var(--color-base-200); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-base-200) 30%, transparent); + } + } + .bg-base-300 { + background-color: var(--color-base-300); + } + .bg-blue-700 { + background-color: var(--color-blue-700); + } + .bg-green-100 { + background-color: var(--color-green-100); + } + .bg-primary { + background-color: var(--color-primary); + } + .bg-primary-content\/10 { + background-color: var(--color-primary-content); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-primary-content) 10%, transparent); + } + } + .bg-transparent { + background-color: transparent; + } + .bg-white { + background-color: var(--color-white); + } + .bg-linear-to-b { + --tw-gradient-position: to bottom; + @supports (background-image: linear-gradient(in lab, red, red)) { + --tw-gradient-position: to bottom in oklab; + } + background-image: linear-gradient(var(--tw-gradient-stops)); + } + .bg-linear-to-br { + --tw-gradient-position: to bottom right; + @supports (background-image: linear-gradient(in lab, red, red)) { + --tw-gradient-position: to bottom right in oklab; + } + background-image: linear-gradient(var(--tw-gradient-stops)); + } + .bg-linear-to-r { + --tw-gradient-position: to right; + @supports (background-image: linear-gradient(in lab, red, red)) { + --tw-gradient-position: to right in oklab; + } + background-image: linear-gradient(var(--tw-gradient-stops)); + } + .from-base-200 { + --tw-gradient-from: var(--color-base-200); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .from-orange-500\/80 { + --tw-gradient-from: color-mix(in srgb, oklch(70.5% 0.213 47.604) 80%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-from: color-mix(in oklab, var(--color-orange-500) 80%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .from-primary { + --tw-gradient-from: var(--color-primary); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .from-primary\/80 { + --tw-gradient-from: var(--color-primary); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-from: color-mix(in oklab, var(--color-primary) 80%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .from-secondary { + --tw-gradient-from: var(--color-secondary); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .from-secondary\/80 { + --tw-gradient-from: var(--color-secondary); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-from: color-mix(in oklab, var(--color-secondary) 80%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .from-success { + --tw-gradient-from: var(--color-success); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .from-warning { + --tw-gradient-from: var(--color-warning); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .via-base-200\/80 { + --tw-gradient-via: var(--color-base-200); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-via: color-mix(in oklab, var(--color-base-200) 80%, transparent); + } + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-via-stops); + } + .to-orange-500\/60 { + --tw-gradient-to: color-mix(in srgb, oklch(70.5% 0.213 47.604) 60%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-to: color-mix(in oklab, var(--color-orange-500) 60%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-primary\/60 { + --tw-gradient-to: var(--color-primary); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-to: color-mix(in oklab, var(--color-primary) 60%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-primary\/80 { + --tw-gradient-to: var(--color-primary); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-to: color-mix(in oklab, var(--color-primary) 80%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-secondary { + --tw-gradient-to: var(--color-secondary); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-secondary\/60 { + --tw-gradient-to: var(--color-secondary); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-to: color-mix(in oklab, var(--color-secondary) 60%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-secondary\/80 { + --tw-gradient-to: var(--color-secondary); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-to: color-mix(in oklab, var(--color-secondary) 80%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-success\/80 { + --tw-gradient-to: var(--color-success); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-to: color-mix(in oklab, var(--color-success) 80%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-transparent { + --tw-gradient-to: transparent; + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-warning\/80 { + --tw-gradient-to: var(--color-warning); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-to: color-mix(in oklab, var(--color-warning) 80%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .loading-bars { + @layer daisyui.l1.l2 { + mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='1' y='1' width='6' height='22'%3E%3Canimate attributeName='y' values='1;5;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite'/%3E%3Canimate attributeName='height' values='22;14;22' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite'/%3E%3Canimate attributeName='opacity' values='1;0.2;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite'/%3E%3C/rect%3E%3Crect x='9' y='1' width='6' height='22'%3E%3Canimate attributeName='y' values='1;5;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.65s'/%3E%3Canimate attributeName='height' values='22;14;22' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.65s'/%3E%3Canimate attributeName='opacity' values='1;0.2;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.65s'/%3E%3C/rect%3E%3Crect x='17' y='1' width='6' height='22'%3E%3Canimate attributeName='y' values='1;5;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.5s'/%3E%3Canimate attributeName='height' values='22;14;22' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.5s'/%3E%3Canimate attributeName='opacity' values='1;0.2;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.5s'/%3E%3C/rect%3E%3C/svg%3E"); + } + } + .mask-squircle { + @layer daisyui.l1.l2 { + mask-image: url("data:image/svg+xml,%3csvg width='200' height='200' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M100 0C20 0 0 20 0 100s20 100 100 100 100-20 100-100S180 0 100 0Z'/%3e%3c/svg%3e"); + } + } + .fill-gray-500 { + fill: var(--color-gray-500); + } + .object-contain { + object-fit: contain; + } + .object-cover { + object-fit: cover; + } + .checkbox-sm { + @layer daisyui.l1.l2 { + padding: 0.1875rem; + --size: calc(var(--size-selector, 0.25rem) * 5); + } + } + .radio-sm { + @layer daisyui.l1.l2 { + padding: 0.1875rem; + &:is([type="radio"]) { + --size: calc(var(--size-selector, 0.25rem) * 5); + } + } + } + .p-0 { + padding: calc(var(--spacing) * 0); + } + .p-1 { + padding: calc(var(--spacing) * 1); + } + .p-1\.5 { + padding: calc(var(--spacing) * 1.5); + } + .p-2 { + padding: calc(var(--spacing) * 2); + } + .p-3 { + padding: calc(var(--spacing) * 3); + } + .p-4 { + padding: calc(var(--spacing) * 4); + } + .p-5 { + padding: calc(var(--spacing) * 5); + } + .p-\[0_5px\] { + padding: 0 5px; + } + .p-\[0_6px\] { + padding: 0 6px; + } + .p-\[0_7px\] { + padding: 0 7px; + } + .p-\[0_8px\] { + padding: 0 8px; + } + .p-\[0_9px\] { + padding: 0 9px; + } + .p-\[0_10px\] { + padding: 0 10px; + } + .p-\[0_10px_0_30px\] { + padding: 0 10px 0 30px; + } + .p-\[0_11px\] { + padding: 0 11px; + } + .p-\[0_12px\] { + padding: 0 12px; + } + .p-\[0_14px\] { + padding: 0 14px; + } + .p-\[0_15px\] { + padding: 0 15px; + } + .p-\[0_16px\] { + padding: 0 16px; + } + .p-\[1px_7px\] { + padding: 1px 7px; + } + .p-\[4px_10px\] { + padding: 4px 10px; + } + .p-\[5px\] { + padding: 5px; + } + .p-\[5px_0\] { + padding: 5px 0; + } + .p-\[5px_10px\] { + padding: 5px 10px; + } + .p-\[5px_10px_5px_10px\] { + padding: 5px 10px 5px 10px; + } + .p-\[6px_10px\] { + padding: 6px 10px; + } + .p-\[6px_16px\] { + padding: 6px 16px; + } + .p-\[7px\] { + padding: 7px; + } + .p-\[7px_10px\] { + padding: 7px 10px; + } + .p-\[8px\] { + padding: 8px; + } + .p-\[8px_10px\] { + padding: 8px 10px; + } + .p-\[8px_12px\] { + padding: 8px 12px; + } + .p-\[8px_13px\] { + padding: 8px 13px; + } + .p-\[10px\] { + padding: 10px; + } + .p-\[10px_10px_15px_13px\] { + padding: 10px 10px 15px 13px; + } + .p-\[10px_15px\] { + padding: 10px 15px; + } + .p-\[11px_16px\] { + padding: 11px 16px; + } + .p-\[12px_20px\] { + padding: 12px 20px; + } + .p-\[15px\] { + padding: 15px; + } + .p-\[16pX_16px_20px\] { + padding: 16pX 16px 20px; + } + .p-\[16px\] { + padding: 16px; + } + .p-\[20px\] { + padding: 20px; + } + .p-\[20px_15px\] { + padding: 20px 15px; + } + .p-\[20px_16px\] { + padding: 20px 16px; + } + .p-\[20px_20px_0_20px\] { + padding: 20px 20px 0 20px; + } + .p-\[_15px\] { + padding: 15px; + } + .menu-title { + @layer daisyui.l1.l2.l3 { + padding-inline: calc(0.25rem * 3); + padding-block: calc(0.25rem * 2); + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 40%, transparent); + } + font-size: 0.875rem; + font-weight: 600; + } + } + .select-md { + @layer daisyui.l1.l2 { + --size: calc(var(--size-field, 0.25rem) * 10); + font-size: 0.875rem; + option { + padding-inline: calc(0.25rem * 3); + padding-block: calc(0.25rem * 1.5); + } + } + } + .select-sm { + @layer daisyui.l1.l2 { + --size: calc(var(--size-field, 0.25rem) * 8); + font-size: 0.75rem; + option { + padding-inline: calc(0.25rem * 2.5); + padding-block: calc(0.25rem * 1); + } + } + } + .table-md { + @layer daisyui.l1.l2 { + :not(thead, tfoot) tr { + font-size: 0.875rem; + } + :where(th, td) { + padding-inline: calc(0.25rem * 4); + padding-block: calc(0.25rem * 3); + } + } + } + .table-sm { + @layer daisyui.l1.l2 { + :not(thead, tfoot) tr { + font-size: 0.75rem; + } + :where(th, td) { + padding-inline: calc(0.25rem * 3); + padding-block: calc(0.25rem * 2); + } + } + } + .px-0 { + padding-inline: calc(var(--spacing) * 0); + } + .px-1 { + padding-inline: calc(var(--spacing) * 1); + } + .px-1\.5 { + padding-inline: calc(var(--spacing) * 1.5); + } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } + .px-2\.5 { + padding-inline: calc(var(--spacing) * 2.5); + } + .px-3 { + padding-inline: calc(var(--spacing) * 3); + } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + .px-5 { + padding-inline: calc(var(--spacing) * 5); + } + .px-6 { + padding-inline: calc(var(--spacing) * 6); + } + .px-\[5px\] { + padding-inline: 5px; + } + .px-\[8px\] { + padding-inline: 8px; + } + .px-\[10px\] { + padding-inline: 10px; + } + .px-\[15px\] { + padding-inline: 15px; + } + .px-\[20px\] { + padding-inline: 20px; + } + .px-\[30px\] { + padding-inline: 30px; + } + .py-0\.5 { + padding-block: calc(var(--spacing) * 0.5); + } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } + .py-1\.5 { + padding-block: calc(var(--spacing) * 1.5); + } + .py-2 { + padding-block: calc(var(--spacing) * 2); + } + .py-3 { + padding-block: calc(var(--spacing) * 3); + } + .py-5 { + padding-block: calc(var(--spacing) * 5); + } + .py-12 { + padding-block: calc(var(--spacing) * 12); + } + .py-\[3px\] { + padding-block: 3px; + } + .py-\[5px\] { + padding-block: 5px; + } + .py-\[6px\] { + padding-block: 6px; + } + .py-\[8px\] { + padding-block: 8px; + } + .py-\[10px\] { + padding-block: 10px; + } + .py-\[15px\] { + padding-block: 15px; + } + .py-\[20px\] { + padding-block: 20px; + } + .ps-2\.5 { + padding-inline-start: calc(var(--spacing) * 2.5); + } + .ps-4 { + padding-inline-start: calc(var(--spacing) * 4); + } + .pe-2 { + padding-inline-end: calc(var(--spacing) * 2); + } + .pt-1 { + padding-top: calc(var(--spacing) * 1); + } + .pt-3 { + padding-top: calc(var(--spacing) * 3); + } + .pt-4 { + padding-top: calc(var(--spacing) * 4); + } + .pt-5 { + padding-top: calc(var(--spacing) * 5); + } + .pt-\[20px\] { + padding-top: 20px; + } + .pb-0 { + padding-bottom: calc(var(--spacing) * 0); + } + .pb-2 { + padding-bottom: calc(var(--spacing) * 2); + } + .pb-\[5px\] { + padding-bottom: 5px; + } + .pb-\[10px\] { + padding-bottom: 10px; + } + .pb-\[14px\] { + padding-bottom: 14px; + } + .pb-\[15px\] { + padding-bottom: 15px; + } + .pl-\[3px\] { + padding-left: 3px; + } + .pl-\[5px\] { + padding-left: 5px; + } + .pl-\[15px\] { + padding-left: 15px; + } + .pl-\[25px\] { + padding-left: 25px; + } + .pl-\[26px\] { + padding-left: 26px; + } + .pl-\[30px\] { + padding-left: 30px; + } + .text-center { + text-align: center; + } + .text-end { + text-align: end; + } + .text-left { + text-align: left; + } + .text-right { + text-align: right; + } + .text-start { + text-align: start; + } + .align-text-top { + vertical-align: text-top; + } + .align-top { + vertical-align: top; + } + .font-mono { + font-family: var(--font-mono); + } + .\!text-sm { + font-size: var(--text-sm) !important; + line-height: var(--tw-leading, var(--text-sm--line-height)) !important; + } + .text-2xl { + font-size: var(--text-2xl); + line-height: var(--tw-leading, var(--text-2xl--line-height)); + } + .text-4xl { + font-size: var(--text-4xl); + line-height: var(--tw-leading, var(--text-4xl--line-height)); + } + .text-base { + font-size: var(--text-base); + line-height: var(--tw-leading, var(--text-base--line-height)); + } + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } + .text-lg\/none { + font-size: var(--text-lg); + line-height: 1; + } + .text-sm { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + .text-sm\/none { + font-size: var(--text-sm); + line-height: 1; + } + .text-xl { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } + .text-xs { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + .text-xs\/none { + font-size: var(--text-xs); + line-height: 1; + } + .tabs-sm { + @layer daisyui.l1.l2 { + --tab-height: calc(var(--size-field, 0.25rem) * 8); + & > .tab { + font-size: 0.875rem; + --tab-p: 0.5rem; + --tab-radius-min: calc(0.5rem - var(--border)); + } + } + } + .tabs-xs { + @layer daisyui.l1.l2 { + --tab-height: calc(var(--size-field, 0.25rem) * 6); + & > .tab { + font-size: 0.75rem; + --tab-p: 0.375rem; + --tab-radius-min: calc(0.5rem - var(--border)); + } + } + } + .badge-sm { + @layer daisyui.l1.l2 { + --size: calc(var(--size-selector, 0.25rem) * 5); + font-size: 0.75rem; + } + } + .text-\[11px\] { + font-size: 11px; + } + .text-\[12px\] { + font-size: 12px; + } + .text-\[13px\] { + font-size: 13px; + } + .text-\[14px\] { + font-size: 14px; + } + .text-\[16px\] { + font-size: 16px; + } + .text-\[18px\] { + font-size: 18px; + } + .text-\[20px\] { + font-size: 20px; + } + .text-\[22px\] { + font-size: 22px; + } + .text-\[24px\] { + font-size: 24px; + } + .text-\[28px\] { + font-size: 28px; + } + .leading-\[10\.2px\] { + --tw-leading: 10.2px; + line-height: 10.2px; + } + .leading-\[10\.3px\] { + --tw-leading: 10.3px; + line-height: 10.3px; + } + .leading-\[10\.4px\] { + --tw-leading: 10.4px; + line-height: 10.4px; + } + .leading-\[10\.5px\] { + --tw-leading: 10.5px; + line-height: 10.5px; + } + .leading-\[10px\] { + --tw-leading: 10px; + line-height: 10px; + } + .leading-\[11px\] { + --tw-leading: 11px; + line-height: 11px; + } + .leading-\[12px\] { + --tw-leading: 12px; + line-height: 12px; + } + .leading-\[16px\] { + --tw-leading: 16px; + line-height: 16px; + } + .leading-\[18px\] { + --tw-leading: 18px; + line-height: 18px; + } + .leading-\[20px\] { + --tw-leading: 20px; + line-height: 20px; + } + .leading-\[23px\] { + --tw-leading: 23px; + line-height: 23px; + } + .leading-\[24px\] { + --tw-leading: 24px; + line-height: 24px; + } + .leading-\[25px\] { + --tw-leading: 25px; + line-height: 25px; + } + .leading-\[26px\] { + --tw-leading: 26px; + line-height: 26px; + } + .leading-\[28px\] { + --tw-leading: 28px; + line-height: 28px; + } + .leading-\[29px\] { + --tw-leading: 29px; + line-height: 29px; + } + .leading-\[30px\] { + --tw-leading: 30px; + line-height: 30px; + } + .leading-\[31px\] { + --tw-leading: 31px; + line-height: 31px; + } + .leading-\[32px\] { + --tw-leading: 32px; + line-height: 32px; + } + .leading-\[33px\] { + --tw-leading: 33px; + line-height: 33px; + } + .leading-\[34\.5px\] { + --tw-leading: 34.5px; + line-height: 34.5px; + } + .leading-\[34px\] { + --tw-leading: 34px; + line-height: 34px; + } + .leading-\[35px\] { + --tw-leading: 35px; + line-height: 35px; + } + .leading-\[36px\] { + --tw-leading: 36px; + line-height: 36px; + } + .leading-\[54px\] { + --tw-leading: 54px; + line-height: 54px; + } + .leading-none { + --tw-leading: 1; + line-height: 1; + } + .leading-tight { + --tw-leading: var(--leading-tight); + line-height: var(--leading-tight); + } + .font-\[16px\] { + --tw-font-weight: 16px; + font-weight: 16px; + } + .font-\[20px\] { + --tw-font-weight: 20px; + font-weight: 20px; + } + .font-\[300\] { + --tw-font-weight: 300; + font-weight: 300; + } + .font-\[400\] { + --tw-font-weight: 400; + font-weight: 400; + } + .font-\[500\] { + --tw-font-weight: 500; + font-weight: 500; + } + .font-\[600\] { + --tw-font-weight: 600; + font-weight: 600; + } + .font-\[700\] { + --tw-font-weight: 700; + font-weight: 700; + } + .font-bold { + --tw-font-weight: var(--font-weight-bold); + font-weight: var(--font-weight-bold); + } + .font-medium { + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + .font-normal { + --tw-font-weight: var(--font-weight-normal); + font-weight: var(--font-weight-normal); + } + .font-semibold { + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + } + .tracking-wider { + --tw-tracking: var(--tracking-wider); + letter-spacing: var(--tracking-wider); + } + .whitespace-nowrap { + white-space: nowrap; + } + .file-input-primary { + @layer daisyui.l1.l2 { + --btn-color: var(--color-primary); + &::file-selector-button { + color: var(--color-primary-content); + } + &, &:focus, &:focus-within { + --input-color: var(--color-primary); + } + } + } + .checkbox-success { + @layer daisyui.l1.l2 { + color: var(--color-success-content); + --input-color: var(--color-success); + } + } + .link-primary { + @layer daisyui.l1.l2 { + color: var(--color-primary); + @media (hover: hover) { + &:hover { + color: var(--color-primary); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-primary) 80%, #000); + } + } + } + } + } + .progress-accent { + @layer daisyui.l1.l2 { + color: var(--color-accent); + } + } + .progress-info { + @layer daisyui.l1.l2 { + color: var(--color-info); + } + } + .progress-primary { + @layer daisyui.l1.l2 { + color: var(--color-primary); + } + } + .progress-secondary { + @layer daisyui.l1.l2 { + color: var(--color-secondary); + } + } + .progress-warning { + @layer daisyui.l1.l2 { + color: var(--color-warning); + } + } + .text-\[\#000\] { + color: #000; + } + .text-\[\#6B7280\] { + color: #6B7280; + } + .text-\[\#7E7E7E\] { + color: #7E7E7E; + } + .text-\[\#9E9E9E\] { + color: #9E9E9E; + } + .text-\[\#9E9E9E_\!important\] { + color: #9E9E9E !important; + } + .text-\[\#0041E8\] { + color: #0041E8; + } + .text-\[\#0041E8_\!important\] { + color: #0041E8 !important; + } + .text-\[\#79B0E2\] { + color: #79B0E2; + } + .text-\[\#79b0e2\] { + color: #79b0e2; + } + .text-\[\#299D00\] { + color: #299D00; + } + .text-\[\#383C44\] { + color: #383C44; + } + .text-\[\#919699\] { + color: #919699; + } + .text-\[\#B1B1B1\] { + color: #B1B1B1; + } + .text-\[\#C5C5C5\] { + color: #C5C5C5; + } + .text-\[\#E00000\] { + color: #E00000; + } + .text-\[\#FFC700\] { + color: #FFC700; + } + .text-\[\#f6f6f6\] { + color: #f6f6f6; + } + .text-\[\#fff\] { + color: #fff; + } + .text-\[\#fff_\!important\] { + color: #fff !important; + } + .text-\[6b7280\] { + color: 6b7280; + } + .text-\[black\] { + color: black; + } + .text-\[red\] { + color: red; + } + .text-base-content { + color: var(--color-base-content); + } + .text-base-content\/50 { + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 50%, transparent); + } + } + .text-base-content\/60 { + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 60%, transparent); + } + } + .text-base-content\/70 { + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 70%, transparent); + } + } + .text-base-content\/80 { + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 80%, transparent); + } + } + .text-black { + color: var(--color-black); + } + .text-blue-600 { + color: var(--color-blue-600); + } + .text-blue-700 { + color: var(--color-blue-700); + } + .text-error { + color: var(--color-error); + } + .text-gray-500 { + color: var(--color-gray-500); + } + .text-gray-900 { + color: var(--color-gray-900); + } + .text-green-500 { + color: var(--color-green-500); + } + .text-green-800 { + color: var(--color-green-800); + } + .text-orange-500 { + color: var(--color-orange-500); + } + .text-primary { + color: var(--color-primary); + } + .text-primary-content { + color: var(--color-primary-content); + } + .text-red-200 { + color: var(--color-red-200); + } + .text-red-400 { + color: var(--color-red-400); + } + .text-red-500 { + color: var(--color-red-500); + } + .text-secondary-content { + color: var(--color-secondary-content); + } + .text-success { + color: var(--color-success); + } + .text-success-content { + color: var(--color-success-content); + } + .text-warning-content { + color: var(--color-warning-content); + } + .text-white { + color: var(--color-white); + } + .uppercase { + text-transform: uppercase; + } + .italic { + font-style: italic; + } + .underline { + text-decoration-line: underline; + } + .opacity-0 { + opacity: 0%; + } + .opacity-70 { + opacity: 70%; + } + .opacity-80 { + opacity: 80%; + } + .opacity-100 { + opacity: 100%; + } + .shadow { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-\[0_-1px_1px_0px_\#0000001A\] { + --tw-shadow: 0 -1px 1px 0px var(--tw-shadow-color, #0000001A); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-\[0_0_0_1px_\#0041E8\] { + --tw-shadow: 0 0 0 1px var(--tw-shadow-color, #0041E8); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-\[0_0_0_1px_\#E00000\] { + --tw-shadow: 0 0 0 1px var(--tw-shadow-color, #E00000); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-\[0_0_1px_0px_\#0000001A\] { + --tw-shadow: 0 0 1px 0px var(--tw-shadow-color, #0000001A); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-\[0_1px_1px_0_\#0000001A\] { + --tw-shadow: 0 1px 1px 0 var(--tw-shadow-color, #0000001A); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-\[0_1px_1px_0_rgba\(0\,0\,0\,0\.1\)\] { + --tw-shadow: 0 1px 1px 0 var(--tw-shadow-color, rgba(0,0,0,0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-\[0_1px_1px_0_rgba\(0\,0\,0\,0\.10\)\] { + --tw-shadow: 0 1px 1px 0 var(--tw-shadow-color, rgba(0,0,0,0.10)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-\[0_1px_1px_0px_\#0000001A\] { + --tw-shadow: 0 1px 1px 0px var(--tw-shadow-color, #0000001A); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-\[0_2px_4px_0_rgba\(0\,0\,0\,0\.12\)\] { + --tw-shadow: 0 2px 4px 0 var(--tw-shadow-color, rgba(0,0,0,0.12)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-\[0px_-1px_1px_0px_\#0000001A\] { + --tw-shadow: 0px -1px 1px 0px var(--tw-shadow-color, #0000001A); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-\[0px_-10px_40px_0px\] { + --tw-shadow: 0px -10px 40px 0px var(--tw-shadow-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-\[0px_1px_1px_0px_\#0000001A\] { + --tw-shadow: 0px 1px 1px 0px var(--tw-shadow-color, #0000001A); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-\[0px_1px_1px_0px_rgba\(0\,0\,0\,0\.10\)\] { + --tw-shadow: 0px 1px 1px 0px var(--tw-shadow-color, rgba(0,0,0,0.10)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-\[0px_2px_4px_0px_\#0000001F\] { + --tw-shadow: 0px 2px 4px 0px var(--tw-shadow-color, #0000001F); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-\[0px_2px_4px_0px_rgba\(0\,0\,0\,0\.10\)\] { + --tw-shadow: 0px 2px 4px 0px var(--tw-shadow-color, rgba(0,0,0,0.10)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-\[0px_2px_4px_0px_rgba\(0\,0\,0\,0\.12\)\] { + --tw-shadow: 0px 2px 4px 0px var(--tw-shadow-color, rgba(0,0,0,0.12)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-md { + --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-none { + --tw-shadow: 0 0 #0000; + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-sm { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-xl { + --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-xs { + --tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-base-content\/4 { + --tw-shadow-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-base-content) 4%, transparent) var(--tw-shadow-alpha), transparent); + } + } + .outline-\[0px\] { + outline-style: var(--tw-outline-style); + outline-width: 0px; + } + .btn-ghost { + @layer daisyui.l1 { + &:not(.btn-active, :hover, :active:focus, :focus-visible, input:checked:not(.filter .btn)) { + --btn-shadow: ""; + --btn-bg: #0000; + --btn-border: #0000; + --btn-noise: none; + &:not(:disabled, [disabled], .btn-disabled) { + outline-color: currentcolor; + --btn-fg: var(--btn-color, currentColor); + } + } + @media (hover: none) { + &:not(.btn-active, :active, :focus-visible, input:checked:not(.filter .btn)):hover { + outline-color: currentcolor; + --btn-shadow: ""; + --btn-bg: #0000; + --btn-fg: var(--btn-color, currentColor); + --btn-border: #0000; + --btn-noise: none; + } + } + } + } + .blur { + --tw-blur: blur(8px); + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .grayscale-\[1\] { + --tw-grayscale: grayscale(1); + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .filter { + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .transition { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-all { + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .duration-300 { + --tw-duration: 300ms; + transition-duration: 300ms; + } + .duration-1000 { + --tw-duration: 1000ms; + transition-duration: 1000ms; + } + .btn-outline { + @layer daisyui.l1 { + &:not( .btn-active, :hover, :active:focus, :focus-visible, input:checked:not(.filter .btn), :disabled, [disabled], .btn-disabled ) { + --btn-shadow: ""; + --btn-bg: #0000; + --btn-fg: var(--btn-color); + --btn-border: var(--btn-color); + --btn-noise: none; + } + @media (hover: none) { + &:not(.btn-active, :active, :focus-visible, input:checked:not(.filter .btn)):hover { + --btn-shadow: ""; + --btn-bg: #0000; + --btn-fg: var(--btn-color); + --btn-border: var(--btn-color); + --btn-noise: none; + } + } + } + } + .btn-soft { + @layer daisyui.l1 { + &:not( .btn-active, :hover, :active:focus, :focus-visible, input:checked:not(.filter .btn), :disabled, [disabled], .btn-disabled ) { + --btn-shadow: ""; + --btn-fg: var(--btn-color, var(--color-base-content)); + --btn-bg: var(--btn-color, var(--color-base-content)); + @supports (color: color-mix(in lab, red, red)) { + --btn-bg: color-mix( + in oklab, + var(--btn-color, var(--color-base-content)) 8%, + var(--color-base-100) + ); + } + --btn-border: var(--btn-color, var(--color-base-content)); + @supports (color: color-mix(in lab, red, red)) { + --btn-border: color-mix( + in oklab, + var(--btn-color, var(--color-base-content)) 10%, + var(--color-base-100) + ); + } + --btn-noise: none; + } + @media (hover: none) { + &:not(.btn-active, :active, :focus-visible, input:checked:not(.filter .btn)):hover { + --btn-shadow: ""; + --btn-fg: var(--btn-color, var(--color-base-content)); + --btn-bg: var(--btn-color, var(--color-base-content)); + @supports (color: color-mix(in lab, red, red)) { + --btn-bg: color-mix( + in oklab, + var(--btn-color, var(--color-base-content)) 8%, + var(--color-base-100) + ); + } + --btn-border: var(--btn-color, var(--color-base-content)); + @supports (color: color-mix(in lab, red, red)) { + --btn-border: color-mix( + in oklab, + var(--btn-color, var(--color-base-content)) 10%, + var(--color-base-100) + ); + } + --btn-noise: none; + } + } + } + } + .btn-md { + @layer daisyui.l1.l2 { + --fontsize: 0.875rem; + --btn-p: 1rem; + --size: calc(var(--size-field, 0.25rem) * 10); + } + } + .btn-sm { + @layer daisyui.l1.l2 { + --fontsize: 0.75rem; + --btn-p: 0.75rem; + --size: calc(var(--size-field, 0.25rem) * 8); + } + } + .btn-xs { + @layer daisyui.l1.l2 { + --fontsize: 0.6875rem; + --btn-p: 0.5rem; + --size: calc(var(--size-field, 0.25rem) * 6); + } + } + .\!outline-none { + --tw-outline-style: none !important; + outline-style: none !important; + } + .badge-error { + @layer daisyui.l1.l2 { + --badge-color: var(--color-error); + --badge-fg: var(--color-error-content); + } + } + .badge-info { + @layer daisyui.l1.l2 { + --badge-color: var(--color-info); + --badge-fg: var(--color-info-content); + } + } + .badge-primary { + @layer daisyui.l1.l2 { + --badge-color: var(--color-primary); + --badge-fg: var(--color-primary-content); + } + } + .badge-success { + @layer daisyui.l1.l2 { + --badge-color: var(--color-success); + --badge-fg: var(--color-success-content); + } + } + .badge-warning { + @layer daisyui.l1.l2 { + --badge-color: var(--color-warning); + --badge-fg: var(--color-warning-content); + } + } + .btn-error { + @layer daisyui.l1.l2.l3 { + --btn-color: var(--color-error); + --btn-fg: var(--color-error-content); + } + } + .btn-primary { + @layer daisyui.l1.l2.l3 { + --btn-color: var(--color-primary); + --btn-fg: var(--color-primary-content); + } + } + .\[--chkfg\:white\] { + --chkfg: white; + } + .\*\:cursor-pointer { + :is(& > *) { + cursor: pointer; + } + } + .\*\:text-nowrap { + :is(& > *) { + text-wrap: nowrap; + } + } + .group-hover\:block { + &:is(:where(.group):hover *) { + @media (hover: hover) { + display: block; + } + } + } + .group-hover\:translate-x-0 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + --tw-translate-x: calc(var(--spacing) * 0); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } + } + .group-hover\:border-\[\#fff\] { + &:is(:where(.group):hover *) { + @media (hover: hover) { + border-color: #fff; + } + } + } + .group-hover\:border-white { + &:is(:where(.group):hover *) { + @media (hover: hover) { + border-color: var(--color-white); + } + } + } + .group-hover\:text-black { + &:is(:where(.group):hover *) { + @media (hover: hover) { + color: var(--color-black); + } + } + } + .group-hover\:text-white { + &:is(:where(.group):hover *) { + @media (hover: hover) { + color: var(--color-white); + } + } + } + .group-hover\:opacity-100 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + opacity: 100%; + } + } + } + .group-hover\:filter-none { + &:is(:where(.group):hover *) { + @media (hover: hover) { + filter: none; + } + } + } + .group-has-\[\[id\=layout-sidebar-hover-trigger\]\:checked\]\/html\:flex { + &:is(:where(.group\/html):has(*:is([id=layout-sidebar-hover-trigger]:checked)) *) { + display: flex; + } + } + .group-has-\[\[id\=layout-sidebar-hover-trigger\]\:checked\]\/html\:hidden { + &:is(:where(.group\/html):has(*:is([id=layout-sidebar-hover-trigger]:checked)) *) { + display: none; + } + } + .group-data-\[theme\=dark\]\/html\:translate-y-0 { + &:is(:where(.group\/html)[data-theme="dark"] *) { + --tw-translate-y: calc(var(--spacing) * 0); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } + .group-data-\[theme\=dark\]\/html\:opacity-0 { + &:is(:where(.group\/html)[data-theme="dark"] *) { + opacity: 0%; + } + } + .group-data-\[theme\=dark\]\/html\:opacity-100 { + &:is(:where(.group\/html)[data-theme="dark"] *) { + opacity: 100%; + } + } + .group-data-\[theme\=light\]\/html\:translate-y-0 { + &:is(:where(.group\/html)[data-theme="light"] *) { + --tw-translate-y: calc(var(--spacing) * 0); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } + .group-data-\[theme\=light\]\/html\:opacity-0 { + &:is(:where(.group\/html)[data-theme="light"] *) { + opacity: 0%; + } + } + .group-data-\[theme\=light\]\/html\:opacity-100 { + &:is(:where(.group\/html)[data-theme="light"] *) { + opacity: 100%; + } + } + .checked\:bg-\[\#0041E8\] { + &:checked { + background-color: #0041E8; + } + } + .checked\:bg-blue-500 { + &:checked { + background-color: var(--color-blue-500); + } + } + .focus-within\:border-\[\#0041E8\] { + &:focus-within { + border-color: #0041E8; + } + } + .hover\:border-\[\#0041E8\] { + &:hover { + @media (hover: hover) { + border-color: #0041E8; + } + } + } + .hover\:bg-\[\#0041E8\] { + &:hover { + @media (hover: hover) { + background-color: #0041E8; + } + } + } + .hover\:bg-\[\#79B0E2\] { + &:hover { + @media (hover: hover) { + background-color: #79B0E2; + } + } + } + .hover\:bg-\[\#79b0e2\] { + &:hover { + @media (hover: hover) { + background-color: #79b0e2; + } + } + } + .hover\:bg-\[\#F5F7FF\] { + &:hover { + @media (hover: hover) { + background-color: #F5F7FF; + } + } + } + .hover\:bg-base-200 { + &:hover { + @media (hover: hover) { + background-color: var(--color-base-200); + } + } + } + .hover\:bg-base-200\/20 { + &:hover { + @media (hover: hover) { + background-color: var(--color-base-200); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-base-200) 20%, transparent); + } + } + } + } + .hover\:bg-base-300 { + &:hover { + @media (hover: hover) { + background-color: var(--color-base-300); + } + } + } + .hover\:bg-blue-700 { + &:hover { + @media (hover: hover) { + background-color: var(--color-blue-700); + } + } + } + .hover\:bg-error\/10 { + &:hover { + @media (hover: hover) { + background-color: var(--color-error); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-error) 10%, transparent); + } + } + } + } + .hover\:bg-gray-100 { + &:hover { + @media (hover: hover) { + background-color: var(--color-gray-100); + } + } + } + .hover\:stroke-white { + &:hover { + @media (hover: hover) { + stroke: var(--color-white); + } + } + } + .hover\:text-\[\#0041E8_\!important\] { + &:hover { + @media (hover: hover) { + color: #0041E8 !important; + } + } + } + .hover\:text-\[\#fff\] { + &:hover { + @media (hover: hover) { + color: #fff; + } + } + } + .hover\:text-\[\#fff_\!important\] { + &:hover { + @media (hover: hover) { + color: #fff !important; + } + } + } + .hover\:text-blue-700 { + &:hover { + @media (hover: hover) { + color: var(--color-blue-700); + } + } + } + .hover\:text-white { + &:hover { + @media (hover: hover) { + color: var(--color-white); + } + } + } + .hover\:opacity-95 { + &:hover { + @media (hover: hover) { + opacity: 95%; + } + } + } + .hover\:shadow-\[0px_1px_5px_0px_rgba\(0\,0\,0\,0\.20\)\] { + &:hover { + @media (hover: hover) { + --tw-shadow: 0px 1px 5px 0px var(--tw-shadow-color, rgba(0,0,0,0.20)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + } + .hover\:shadow-lg { + &:hover { + @media (hover: hover) { + --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + } + .data-error\:input-error { + &[data-error] { + @layer daisyui.l1.l2 { + &, &:focus, &:focus-within { + --input-color: var(--color-error); + } + } + } + } + .max-\[1620px\]\:p-\[10px\] { + @media (width < 1620px) { + padding: 10px; + } + } + .max-\[1620px\]\:text-\[22px\] { + @media (width < 1620px) { + font-size: 22px; + } + } + .max-sm\:hidden { + @media (width < 40rem) { + display: none; + } + } + .sm\:dropdown-end { + @media (width >= 40rem) { + @layer daisyui.l1.l2 { + --anchor-h: span-left; + :where(.dropdown-content) { + inset-inline-end: calc(0.25rem * 0); + translate: 0 0; + [dir="rtl"] & { + translate: 0 0; + } + } + &.dropdown-left { + --anchor-h: left; + --anchor-v: span-top; + .dropdown-content { + top: auto; + bottom: calc(0.25rem * 0); + } + } + &.dropdown-right { + --anchor-h: right; + --anchor-v: span-top; + .dropdown-content { + top: auto; + bottom: calc(0.25rem * 0); + } + } + } + } + } + .sm\:col-span-2 { + @media (width >= 40rem) { + grid-column: span 2 / span 2; + } + } + .sm\:mx-5 { + @media (width >= 40rem) { + margin-inline: calc(var(--spacing) * 5); + } + } + .sm\:mt-4 { + @media (width >= 40rem) { + margin-top: calc(var(--spacing) * 4); + } + } + .sm\:mt-6 { + @media (width >= 40rem) { + margin-top: calc(var(--spacing) * 6); + } + } + .sm\:mt-8 { + @media (width >= 40rem) { + margin-top: calc(var(--spacing) * 8); + } + } + .sm\:mr-\[0\] { + @media (width >= 40rem) { + margin-right: 0; + } + } + .sm\:block { + @media (width >= 40rem) { + display: block; + } + } + .sm\:size-6 { + @media (width >= 40rem) { + width: calc(var(--spacing) * 6); + height: calc(var(--spacing) * 6); + } + } + .sm\:w-84 { + @media (width >= 40rem) { + width: calc(var(--spacing) * 84); + } + } + .sm\:w-full { + @media (width >= 40rem) { + width: 100%; + } + } + .sm\:footer-horizontal { + @media (width >= 40rem) { + @layer daisyui.l1.l2 { + grid-auto-flow: column; + &.footer-center { + grid-auto-flow: row dense; + } + } + } + } + .sm\:grid-cols-3 { + @media (width >= 40rem) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + } + .sm\:divide-x { + @media (width >= 40rem) { + :where(& > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(1px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse))); + } + } + } + .sm\:p-2\.5 { + @media (width >= 40rem) { + padding: calc(var(--spacing) * 2.5); + } + } + .sm\:p-6 { + @media (width >= 40rem) { + padding: calc(var(--spacing) * 6); + } + } + .sm\:pt-8 { + @media (width >= 40rem) { + padding-top: calc(var(--spacing) * 8); + } + } + .sm\:text-lg { + @media (width >= 40rem) { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } + } + .sm\:whitespace-nowrap { + @media (width >= 40rem) { + white-space: nowrap; + } + } + .md\:mt-\[10px\] { + @media (width >= 48rem) { + margin-top: 10px; + } + } + .md\:flex { + @media (width >= 48rem) { + display: flex; + } + } + .md\:hidden { + @media (width >= 48rem) { + display: none; + } + } + .md\:size-24 { + @media (width >= 48rem) { + width: calc(var(--spacing) * 24); + height: calc(var(--spacing) * 24); + } + } + .md\:w-\[100\%\] { + @media (width >= 48rem) { + width: 100%; + } + } + .md\:justify-start { + @media (width >= 48rem) { + justify-content: flex-start; + } + } + .md\:whitespace-nowrap { + @media (width >= 48rem) { + white-space: nowrap; + } + } + .lg\:grid-cols-2 { + @media (width >= 64rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + .lg\:whitespace-nowrap { + @media (width >= 64rem) { + white-space: nowrap; + } + } + .xl\:col-span-5 { + @media (width >= 80rem) { + grid-column: span 5 / span 5; + } + } + .xl\:col-span-7 { + @media (width >= 80rem) { + grid-column: span 7 / span 7; + } + } + .xl\:w-\[120px\] { + @media (width >= 80rem) { + width: 120px; + } + } + .xl\:grid-cols-4 { + @media (width >= 80rem) { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + } + .xl\:grid-cols-12 { + @media (width >= 80rem) { + grid-template-columns: repeat(12, minmax(0, 1fr)); + } + } + .\32 xl\:col-span-1 { + @media (width >= 96rem) { + grid-column: span 1 / span 1; + } + } + .\32 xl\:mr-\[16px\] { + @media (width >= 96rem) { + margin-right: 16px; + } + } + .\32 xl\:w-\[30\%\] { + @media (width >= 96rem) { + width: 30%; + } + } + .\32 xl\:w-\[50\%\] { + @media (width >= 96rem) { + width: 50%; + } + } + .\32 xl\:w-\[calc\(100\%_-30\%_-16px\)\] { + @media (width >= 96rem) { + width: calc(100% - 30% - 16px); + } + } + .\32 xl\:w-auto { + @media (width >= 96rem) { + width: auto; + } + } + .\32 xl\:justify-end { + @media (width >= 96rem) { + justify-content: flex-end; + } + } + .\32 xl\:whitespace-normal { + @media (width >= 96rem) { + white-space: normal; + } + } + .rtl\:rotate-180 { + &:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *) { + rotate: 180deg; + } + } + .dark\:border-gray-700 { + @media (prefers-color-scheme: dark) { + border-color: var(--color-gray-700); + } + } + .dark\:bg-green-900 { + @media (prefers-color-scheme: dark) { + background-color: var(--color-green-900); + } + } + .dark\:fill-gray-400 { + @media (prefers-color-scheme: dark) { + fill: var(--color-gray-400); + } + } + .dark\:text-gray-400 { + @media (prefers-color-scheme: dark) { + color: var(--color-gray-400); + } + } + .dark\:text-green-300 { + @media (prefers-color-scheme: dark) { + color: var(--color-green-300); + } + } + .dark\:text-green-500 { + @media (prefers-color-scheme: dark) { + color: var(--color-green-500); + } + } + .dark\:text-white { + @media (prefers-color-scheme: dark) { + color: var(--color-white); + } + } + .dark\:hover\:bg-gray-700 { + @media (prefers-color-scheme: dark) { + &:hover { + @media (hover: hover) { + background-color: var(--color-gray-700); + } + } + } + } + .dark\:hover\:text-blue-500 { + @media (prefers-color-scheme: dark) { + &:hover { + @media (hover: hover) { + color: var(--color-blue-500); + } + } + } + } + .dark\:focus\:ring-gray-700 { + @media (prefers-color-scheme: dark) { + &:focus { + --tw-ring-color: var(--color-gray-700); + } + } + } +} +@layer base { + :where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light] { + color-scheme: light; + --color-base-100: oklch(100% 0 0); + --color-base-200: oklch(98% 0 0); + --color-base-300: oklch(95% 0 0); + --color-base-content: oklch(21% 0.006 285.885); + --color-primary: oklch(45% 0.24 277.023); + --color-primary-content: oklch(93% 0.034 272.788); + --color-secondary: oklch(65% 0.241 354.308); + --color-secondary-content: oklch(94% 0.028 342.258); + --color-accent: oklch(77% 0.152 181.912); + --color-accent-content: oklch(38% 0.063 188.416); + --color-neutral: oklch(14% 0.005 285.823); + --color-neutral-content: oklch(92% 0.004 286.32); + --color-info: oklch(74% 0.16 232.661); + --color-info-content: oklch(29% 0.066 243.157); + --color-success: oklch(76% 0.177 163.223); + --color-success-content: oklch(37% 0.077 168.94); + --color-warning: oklch(82% 0.189 84.429); + --color-warning-content: oklch(41% 0.112 45.904); + --color-error: oklch(71% 0.194 13.428); + --color-error-content: oklch(27% 0.105 12.094); + --radius-selector: 0.5rem; + --radius-field: 0.25rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; + } +} +@layer base { + @media (prefers-color-scheme: dark) { + :root:not([data-theme]) { + color-scheme: dark; + --color-base-100: oklch(25.33% 0.016 252.42); + --color-base-200: oklch(23.26% 0.014 253.1); + --color-base-300: oklch(21.15% 0.012 254.09); + --color-base-content: oklch(97.807% 0.029 256.847); + --color-primary: oklch(58% 0.233 277.117); + --color-primary-content: oklch(96% 0.018 272.314); + --color-secondary: oklch(65% 0.241 354.308); + --color-secondary-content: oklch(94% 0.028 342.258); + --color-accent: oklch(77% 0.152 181.912); + --color-accent-content: oklch(38% 0.063 188.416); + --color-neutral: oklch(14% 0.005 285.823); + --color-neutral-content: oklch(92% 0.004 286.32); + --color-info: oklch(74% 0.16 232.661); + --color-info-content: oklch(29% 0.066 243.157); + --color-success: oklch(76% 0.177 163.223); + --color-success-content: oklch(37% 0.077 168.94); + --color-warning: oklch(82% 0.189 84.429); + --color-warning-content: oklch(41% 0.112 45.904); + --color-error: oklch(71% 0.194 13.428); + --color-error-content: oklch(27% 0.105 12.094); + --radius-selector: 0.5rem; + --radius-field: 0.25rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; + } + } +} +@layer base { + :root:has(input.theme-controller[value=light]:checked),[data-theme=light] { + color-scheme: light; + --color-base-100: oklch(100% 0 0); + --color-base-200: oklch(98% 0 0); + --color-base-300: oklch(95% 0 0); + --color-base-content: oklch(21% 0.006 285.885); + --color-primary: oklch(45% 0.24 277.023); + --color-primary-content: oklch(93% 0.034 272.788); + --color-secondary: oklch(65% 0.241 354.308); + --color-secondary-content: oklch(94% 0.028 342.258); + --color-accent: oklch(77% 0.152 181.912); + --color-accent-content: oklch(38% 0.063 188.416); + --color-neutral: oklch(14% 0.005 285.823); + --color-neutral-content: oklch(92% 0.004 286.32); + --color-info: oklch(74% 0.16 232.661); + --color-info-content: oklch(29% 0.066 243.157); + --color-success: oklch(76% 0.177 163.223); + --color-success-content: oklch(37% 0.077 168.94); + --color-warning: oklch(82% 0.189 84.429); + --color-warning-content: oklch(41% 0.112 45.904); + --color-error: oklch(71% 0.194 13.428); + --color-error-content: oklch(27% 0.105 12.094); + --radius-selector: 0.5rem; + --radius-field: 0.25rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; + } +} +@layer base { + :root:has(input.theme-controller[value=dark]:checked),[data-theme=dark] { + color-scheme: dark; + --color-base-100: oklch(25.33% 0.016 252.42); + --color-base-200: oklch(23.26% 0.014 253.1); + --color-base-300: oklch(21.15% 0.012 254.09); + --color-base-content: oklch(97.807% 0.029 256.847); + --color-primary: oklch(58% 0.233 277.117); + --color-primary-content: oklch(96% 0.018 272.314); + --color-secondary: oklch(65% 0.241 354.308); + --color-secondary-content: oklch(94% 0.028 342.258); + --color-accent: oklch(77% 0.152 181.912); + --color-accent-content: oklch(38% 0.063 188.416); + --color-neutral: oklch(14% 0.005 285.823); + --color-neutral-content: oklch(92% 0.004 286.32); + --color-info: oklch(74% 0.16 232.661); + --color-info-content: oklch(29% 0.066 243.157); + --color-success: oklch(76% 0.177 163.223); + --color-success-content: oklch(37% 0.077 168.94); + --color-warning: oklch(82% 0.189 84.429); + --color-warning-content: oklch(41% 0.112 45.904); + --color-error: oklch(71% 0.194 13.428); + --color-error-content: oklch(27% 0.105 12.094); + --radius-selector: 0.5rem; + --radius-field: 0.25rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; + } +} +@layer base { + :root { + --fx-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.34' numOctaves='4' stitchTiles='stitch'%3E%3C/feTurbulence%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23a)' opacity='0.2'%3E%3C/rect%3E%3C/svg%3E"); + } +} +@layer base { + :root { + scrollbar-color: currentColor #0000; + @supports (color: color-mix(in lab, red, red)) { + scrollbar-color: color-mix(in oklch, currentColor 35%, #0000) #0000; + } + } +} +@layer base { + @property --radialprogress { + syntax: ""; + inherits: true; + initial-value: 0%; + } +} +@layer base { + :root:not(span) { + overflow: var(--page-overflow); + } +} +@layer base { + :root { + --page-scroll-bg-on: linear-gradient(var(--root-bg), var(--root-bg)) + var(--root-bg); + @supports (color: color-mix(in lab, red, red)) { + --page-scroll-bg-on: linear-gradient(var(--root-bg), var(--root-bg)) + color-mix(in srgb, var(--root-bg), oklch(0% 0 0) calc(var(--page-has-backdrop, 0) * 40%)); + } + --page-scroll-transition-on: background-color 0.3s ease-out; + transition: var(--page-scroll-transition); + scrollbar-gutter: var(--page-scroll-gutter, unset); + scrollbar-gutter: if(style(--page-has-scroll: 1): var(--page-scroll-gutter, unset) ; else: unset); + } + :root:root { + background: var(--page-scroll-bg, var(--root-bg, var(--color-base-100))); + } + @keyframes set-page-has-scroll { + 0%, to { + --page-has-scroll: 1; + } + } +} +@layer base { + :root, [data-theme] { + background-color: var(--root-bg, var(--color-base-100)); + color: var(--color-base-content); + } + :where(:root, [data-theme]) { + --root-bg: var(--color-base-100); + } +} +@keyframes rating { + 0%, 40% { + scale: 1.1; + filter: brightness(1.05) contrast(1.05); + } +} +@keyframes dropdown { + 0% { + opacity: 0; + } +} +@keyframes radio { + 0% { + padding: 5px; + } + 50% { + padding: 3px; + } +} +@keyframes toast { + 0% { + scale: 0.9; + opacity: 0; + } + 100% { + scale: 1; + opacity: 1; + } +} +@keyframes rotator { + 89.9999%, 100% { + --first-item-position: 0 0%; + } + 90%, 99.9999% { + --first-item-position: 0 calc(var(--items) * 100%); + } + 100% { + translate: 0 -100%; + } +} +@keyframes skeleton { + 0% { + background-position: 150%; + } + 100% { + background-position: -50%; + } +} +@keyframes menu { + 0% { + opacity: 0; + } +} +@keyframes progress { + 50% { + background-position-x: -115%; + } +} +@property --tw-translate-x { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-z { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-rotate-x { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-y { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-z { + syntax: "*"; + inherits: false; +} +@property --tw-skew-x { + syntax: "*"; + inherits: false; +} +@property --tw-skew-y { + syntax: "*"; + inherits: false; +} +@property --tw-space-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-gradient-position { + syntax: "*"; + inherits: false; +} +@property --tw-gradient-from { + syntax: ""; + inherits: false; + initial-value: #0000; +} +@property --tw-gradient-via { + syntax: ""; + inherits: false; + initial-value: #0000; +} +@property --tw-gradient-to { + syntax: ""; + inherits: false; + initial-value: #0000; +} +@property --tw-gradient-stops { + syntax: "*"; + inherits: false; +} +@property --tw-gradient-via-stops { + syntax: "*"; + inherits: false; +} +@property --tw-gradient-from-position { + syntax: ""; + inherits: false; + initial-value: 0%; +} +@property --tw-gradient-via-position { + syntax: ""; + inherits: false; + initial-value: 50%; +} +@property --tw-gradient-to-position { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-leading { + syntax: "*"; + inherits: false; +} +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} +@property --tw-tracking { + syntax: "*"; + inherits: false; +} +@property --tw-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-inset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-ring-inset { + syntax: "*"; + inherits: false; +} +@property --tw-ring-offset-width { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --tw-ring-offset-color { + syntax: "*"; + inherits: false; + initial-value: #fff; +} +@property --tw-ring-offset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-blur { + syntax: "*"; + inherits: false; +} +@property --tw-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-invert { + syntax: "*"; + inherits: false; +} +@property --tw-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} +@property --tw-duration { + syntax: "*"; + inherits: false; +} +@property --tw-divide-x-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} +@keyframes ping { + 75%, 100% { + transform: scale(2); + opacity: 0; + } +} +@layer properties { + @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + *, ::before, ::after, ::backdrop { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-translate-z: 0; + --tw-rotate-x: initial; + --tw-rotate-y: initial; + --tw-rotate-z: initial; + --tw-skew-x: initial; + --tw-skew-y: initial; + --tw-space-y-reverse: 0; + --tw-border-style: solid; + --tw-gradient-position: initial; + --tw-gradient-from: #0000; + --tw-gradient-via: #0000; + --tw-gradient-to: #0000; + --tw-gradient-stops: initial; + --tw-gradient-via-stops: initial; + --tw-gradient-from-position: 0%; + --tw-gradient-via-position: 50%; + --tw-gradient-to-position: 100%; + --tw-leading: initial; + --tw-font-weight: initial; + --tw-tracking: initial; + --tw-shadow: 0 0 #0000; + --tw-shadow-color: initial; + --tw-shadow-alpha: 100%; + --tw-inset-shadow: 0 0 #0000; + --tw-inset-shadow-color: initial; + --tw-inset-shadow-alpha: 100%; + --tw-ring-color: initial; + --tw-ring-shadow: 0 0 #0000; + --tw-inset-ring-color: initial; + --tw-inset-ring-shadow: 0 0 #0000; + --tw-ring-inset: initial; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-offset-shadow: 0 0 #0000; + --tw-outline-style: solid; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-duration: initial; + --tw-divide-x-reverse: 0; + } + } +} diff --git a/assets/script/hura.global.css b/assets/script/hura.global.css new file mode 100644 index 0000000..9daf94e --- /dev/null +++ b/assets/script/hura.global.css @@ -0,0 +1,252 @@ +/* =========================== + FONT IMPORT (Dùng chung) + =========================== */ +@import "https://fonts.googleapis.com/css2?family=DM+Sans:wght@100;200;300;400;500;600;700;800;900;1000&display=swap"; +@import "https://fonts.googleapis.com/css2?family=Wix+Madefor+Text:wght@400;500;600;700;800;1000&display=swap"; +@import "https://fonts.googleapis.com/css2?family=Inclusive+Sans:wght@400;500;600;700;800;900;1000&display=swap"; +@import "https://fonts.googleapis.com/css2?family=AR+One+Sans:wght@400;500;600;700;800;1000&display=swap"; + +@layer properties { + :root { + --font-sans: "Inclusive Sans", sans-serif; + --text-base: 16px; + --bg-body: #fafbfd; + --color-hura: #0041e8; + --color-header: #002bb5; + --text-base--line-height: 1.5; + --color-black: #000; + --color-white: #fff; + --spacing: 0.25rem; + --layout-topbar-background: #fff; + --menu-active: #e9f2ff; + --menu-text: #177bff; + --menu-sub-bg: #fff; + } +} + +@layer base { + html, + body { + font-family: var(--font-sans); + font-size: var(--text-base); + line-height: var(--text-base--line-height); + } + *, + *::before, + *::after { + box-sizing: border-box; + } +} + +@layer components { + #layout-topbar { + background: var(--layout-topbar-background); + top: calc(var(--spacing) * 0); + z-index: 10; + transition-property: top, margin, border-radius; + transition-timing-function: var( + --tw-ease, + var(--default-transition-timing-function) + ); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + --tw-duration: 0.3s; + transition-duration: 0.3s; + position: sticky; + border-bottom-style: solid; + border-bottom-width: 1px; + border-color: var(--color-base-300); + } + #layout-content { + padding: calc(var(--spacing) * 6); + transition-property: all; + transition-timing-function: var( + --tw-ease, + var(--default-transition-timing-function) + ); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + flex-grow: 1; + background: var(--bg-body); + } + .bg-menu { + background-color: var(--color-header); + } + /* menu */ + .menu { + & { + --menu-active-bg: #e9f2ff; + } + } + .menu-horizontal { + display: inline-flex; + flex-direction: row; + } + .menu-horizontal li { + position: relative; + } + + /* ACTIVE STATE (LEVEL 1) */ + .menu-horizontal li.active { + background: var(--menu-active); + } + + .menu-horizontal li.active .grow, + .menu-horizontal li.active svg { + color: var(--menu-text); + } + + /* ACTIVE STATE (LEVEL 2) */ + .menu-horizontal li.active ul li.active { + background: var(--color-hura); + } + + .menu-horizontal li.active ul li.active a { + color: #fff; + } + /* HOVER LEVEL 1 */ + + .menu-horizontal li:hover { + background: var(--menu-active); + } + + .menu-horizontal li:hover .grow, + .menu-horizontal li:hover svg { + color: var(--color-hura); + } + + /* Dải hover giữ submenu không bị mất */ + .menu-horizontal li::before { + content: ""; + position: absolute; + left: 0; + bottom: -18px; + width: 100%; + height: 30px; + display: none; + } + + .menu-horizontal li:hover::before { + display: block; + } + + /* ========================== + SUBMENU (UL) + ========================== */ + .menu-horizontal li .submenu { + position: absolute !important; + top: 125%; + left: 0; + background: var(--menu-sub-bg); + margin-left: 0 !important; + padding: 0.5rem; + z-index: 999; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + max-height: 500px; + overflow-y: auto; + overflow-x: hidden; + display: none; + margin-top: 0 !important; + } + + /* DaisyUI details submenu spacing */ + .menu-horizontal > li:not(.menu-title) > details > ul { + margin-top: 1rem; + padding-inline-end: 0.5rem; + } + + /* SHOW SUBMENU ON HOVER */ + .menu-horizontal li:hover .submenu, + .menu-horizontal li:hover > details > ul { + display: block; + } + + /* Remove extra before from DaisyUI */ + .menu-horizontal > li > details > ul:before { + content: none; + } + /* ========================== + SUBMENU HOVER ITEMS + ========================== */ + .menu-horizontal li ul li:hover { + background: var(--color-hura); + } + + .menu-horizontal li ul li:hover a { + color: #fff; + } + + .menu-horizontal li ul li:hover::before { + display: none; + } + /* Font weight chung */ + .menu-horizontal .grow { + font-weight: 500; + } + .menu-icon-color { + color: #838383; + } + .card-body { + padding: calc(var(--spacing) * 4); + } + .choices { + margin-top: calc(var(--spacing) * 2); + --tw-shadow: + 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), + 0 1px 2px -1px var(--tw-shadow-color, #0000001a); + box-shadow: + var(--tw-inset-shadow), var(--tw-inset-ring-shadow), + var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + border-radius: var(--radius-sm); + } + .choices__inner { + background: #fff !important; + min-height: 38px !important; + padding: 5px !important; + } +} + +@layer hura { + .hura-btn-active { + --btn-bg: var(--color-hura); + --btn-fg: #ffffff; + --btn-border: var(--color-hura); + color: var(--color-white); + } + .hura-btn-active:hover { + --btn-bg: #002bb5; + --btn-border: #002bb5; + } + .hura-btn-outline { + background: #f5f7ff; + color: var(--color-hura); + border: 1px solid var(--color-hura); + } + .hura-btn-outline:hover { + background: var(--color-hura); + color: #fff; + } + .hura-checkbox:checked, + .hura-checkbox[aria-checked="true"] { + background: var(--color-hura); + color: #fff; + } + .hura-radio:checked, + .hura-radio[aria-checked="true"] { + border-color: var(--color-hura); + } + .hura-radio:checked::before, + .hura-radio[aria-checked="true"]::before { + background-color: var(--color-hura); + } + .hura-text-blue { + color: var(--color-hura); + } + .hura-bg-blue { + background: var(--color-hura); + } + .hura-shadow-blue { + box-shadow: + 0 10px 15px -3px rgb(var(--color-hura) / 0.35), + 0 4px 6px -4px rgb(var(--color-hura) / 0.25); + } +} diff --git a/assets/script/jquery.js b/assets/script/jquery.js new file mode 100644 index 0000000..ab3d6c1 --- /dev/null +++ b/assets/script/jquery.js @@ -0,0 +1,2 @@ +/*! jQuery v3.7.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function (e, t) { "use strict"; "object" == typeof module && "object" == typeof module.exports ? module.exports = e.document ? t(e, !0) : function (e) { if (!e.document) throw new Error("jQuery requires a window with a document"); return t(e) } : t(e) }("undefined" != typeof window ? window : this, function (ie, e) { "use strict"; var oe = [], r = Object.getPrototypeOf, ae = oe.slice, g = oe.flat ? function (e) { return oe.flat.call(e) } : function (e) { return oe.concat.apply([], e) }, s = oe.push, se = oe.indexOf, n = {}, i = n.toString, ue = n.hasOwnProperty, o = ue.toString, a = o.call(Object), le = {}, v = function (e) { return "function" == typeof e && "number" != typeof e.nodeType && "function" != typeof e.item }, y = function (e) { return null != e && e === e.window }, C = ie.document, u = { type: !0, src: !0, nonce: !0, noModule: !0 }; function m(e, t, n) { var r, i, o = (n = n || C).createElement("script"); if (o.text = e, t) for (r in u) (i = t[r] || t.getAttribute && t.getAttribute(r)) && o.setAttribute(r, i); n.head.appendChild(o).parentNode.removeChild(o) } function x(e) { return null == e ? e + "" : "object" == typeof e || "function" == typeof e ? n[i.call(e)] || "object" : typeof e } var t = "3.7.1", l = /HTML$/i, ce = function (e, t) { return new ce.fn.init(e, t) }; function c(e) { var t = !!e && "length" in e && e.length, n = x(e); return !v(e) && !y(e) && ("array" === n || 0 === t || "number" == typeof t && 0 < t && t - 1 in e) } function fe(e, t) { return e.nodeName && e.nodeName.toLowerCase() === t.toLowerCase() } ce.fn = ce.prototype = { jquery: t, constructor: ce, length: 0, toArray: function () { return ae.call(this) }, get: function (e) { return null == e ? ae.call(this) : e < 0 ? this[e + this.length] : this[e] }, pushStack: function (e) { var t = ce.merge(this.constructor(), e); return t.prevObject = this, t }, each: function (e) { return ce.each(this, e) }, map: function (n) { return this.pushStack(ce.map(this, function (e, t) { return n.call(e, t, e) })) }, slice: function () { return this.pushStack(ae.apply(this, arguments)) }, first: function () { return this.eq(0) }, last: function () { return this.eq(-1) }, even: function () { return this.pushStack(ce.grep(this, function (e, t) { return (t + 1) % 2 })) }, odd: function () { return this.pushStack(ce.grep(this, function (e, t) { return t % 2 })) }, eq: function (e) { var t = this.length, n = +e + (e < 0 ? t : 0); return this.pushStack(0 <= n && n < t ? [this[n]] : []) }, end: function () { return this.prevObject || this.constructor() }, push: s, sort: oe.sort, splice: oe.splice }, ce.extend = ce.fn.extend = function () { var e, t, n, r, i, o, a = arguments[0] || {}, s = 1, u = arguments.length, l = !1; for ("boolean" == typeof a && (l = a, a = arguments[s] || {}, s++), "object" == typeof a || v(a) || (a = {}), s === u && (a = this, s--); s < u; s++)if (null != (e = arguments[s])) for (t in e) r = e[t], "__proto__" !== t && a !== r && (l && r && (ce.isPlainObject(r) || (i = Array.isArray(r))) ? (n = a[t], o = i && !Array.isArray(n) ? [] : i || ce.isPlainObject(n) ? n : {}, i = !1, a[t] = ce.extend(l, o, r)) : void 0 !== r && (a[t] = r)); return a }, ce.extend({ expando: "jQuery" + (t + Math.random()).replace(/\D/g, ""), isReady: !0, error: function (e) { throw new Error(e) }, noop: function () { }, isPlainObject: function (e) { var t, n; return !(!e || "[object Object]" !== i.call(e)) && (!(t = r(e)) || "function" == typeof (n = ue.call(t, "constructor") && t.constructor) && o.call(n) === a) }, isEmptyObject: function (e) { var t; for (t in e) return !1; return !0 }, globalEval: function (e, t, n) { m(e, { nonce: t && t.nonce }, n) }, each: function (e, t) { var n, r = 0; if (c(e)) { for (n = e.length; r < n; r++)if (!1 === t.call(e[r], r, e[r])) break } else for (r in e) if (!1 === t.call(e[r], r, e[r])) break; return e }, text: function (e) { var t, n = "", r = 0, i = e.nodeType; if (!i) while (t = e[r++]) n += ce.text(t); return 1 === i || 11 === i ? e.textContent : 9 === i ? e.documentElement.textContent : 3 === i || 4 === i ? e.nodeValue : n }, makeArray: function (e, t) { var n = t || []; return null != e && (c(Object(e)) ? ce.merge(n, "string" == typeof e ? [e] : e) : s.call(n, e)), n }, inArray: function (e, t, n) { return null == t ? -1 : se.call(t, e, n) }, isXMLDoc: function (e) { var t = e && e.namespaceURI, n = e && (e.ownerDocument || e).documentElement; return !l.test(t || n && n.nodeName || "HTML") }, merge: function (e, t) { for (var n = +t.length, r = 0, i = e.length; r < n; r++)e[i++] = t[r]; return e.length = i, e }, grep: function (e, t, n) { for (var r = [], i = 0, o = e.length, a = !n; i < o; i++)!t(e[i], i) !== a && r.push(e[i]); return r }, map: function (e, t, n) { var r, i, o = 0, a = []; if (c(e)) for (r = e.length; o < r; o++)null != (i = t(e[o], o, n)) && a.push(i); else for (o in e) null != (i = t(e[o], o, n)) && a.push(i); return g(a) }, guid: 1, support: le }), "function" == typeof Symbol && (ce.fn[Symbol.iterator] = oe[Symbol.iterator]), ce.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "), function (e, t) { n["[object " + t + "]"] = t.toLowerCase() }); var pe = oe.pop, de = oe.sort, he = oe.splice, ge = "[\\x20\\t\\r\\n\\f]", ve = new RegExp("^" + ge + "+|((?:^|[^\\\\])(?:\\\\.)*)" + ge + "+$", "g"); ce.contains = function (e, t) { var n = t && t.parentNode; return e === n || !(!n || 1 !== n.nodeType || !(e.contains ? e.contains(n) : e.compareDocumentPosition && 16 & e.compareDocumentPosition(n))) }; var f = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; function p(e, t) { return t ? "\0" === e ? "\ufffd" : e.slice(0, -1) + "\\" + e.charCodeAt(e.length - 1).toString(16) + " " : "\\" + e } ce.escapeSelector = function (e) { return (e + "").replace(f, p) }; var ye = C, me = s; !function () { var e, b, w, o, a, T, r, C, d, i, k = me, S = ce.expando, E = 0, n = 0, s = W(), c = W(), u = W(), h = W(), l = function (e, t) { return e === t && (a = !0), 0 }, f = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", t = "(?:\\\\[\\da-fA-F]{1,6}" + ge + "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", p = "\\[" + ge + "*(" + t + ")(?:" + ge + "*([*^$|!~]?=)" + ge + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + t + "))|)" + ge + "*\\]", g = ":(" + t + ")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|" + p + ")*)|.*)\\)|)", v = new RegExp(ge + "+", "g"), y = new RegExp("^" + ge + "*," + ge + "*"), m = new RegExp("^" + ge + "*([>+~]|" + ge + ")" + ge + "*"), x = new RegExp(ge + "|>"), j = new RegExp(g), A = new RegExp("^" + t + "$"), D = { ID: new RegExp("^#(" + t + ")"), CLASS: new RegExp("^\\.(" + t + ")"), TAG: new RegExp("^(" + t + "|[*])"), ATTR: new RegExp("^" + p), PSEUDO: new RegExp("^" + g), CHILD: new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + ge + "*(even|odd|(([+-]|)(\\d*)n|)" + ge + "*(?:([+-]|)" + ge + "*(\\d+)|))" + ge + "*\\)|)", "i"), bool: new RegExp("^(?:" + f + ")$", "i"), needsContext: new RegExp("^" + ge + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + ge + "*((?:-\\d)?\\d*)" + ge + "*\\)|)(?=[^-]|$)", "i") }, N = /^(?:input|select|textarea|button)$/i, q = /^h\d$/i, L = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, H = /[+~]/, O = new RegExp("\\\\[\\da-fA-F]{1,6}" + ge + "?|\\\\([^\\r\\n\\f])", "g"), P = function (e, t) { var n = "0x" + e.slice(1) - 65536; return t || (n < 0 ? String.fromCharCode(n + 65536) : String.fromCharCode(n >> 10 | 55296, 1023 & n | 56320)) }, M = function () { V() }, R = J(function (e) { return !0 === e.disabled && fe(e, "fieldset") }, { dir: "parentNode", next: "legend" }); try { k.apply(oe = ae.call(ye.childNodes), ye.childNodes), oe[ye.childNodes.length].nodeType } catch (e) { k = { apply: function (e, t) { me.apply(e, ae.call(t)) }, call: function (e) { me.apply(e, ae.call(arguments, 1)) } } } function I(t, e, n, r) { var i, o, a, s, u, l, c, f = e && e.ownerDocument, p = e ? e.nodeType : 9; if (n = n || [], "string" != typeof t || !t || 1 !== p && 9 !== p && 11 !== p) return n; if (!r && (V(e), e = e || T, C)) { if (11 !== p && (u = L.exec(t))) if (i = u[1]) { if (9 === p) { if (!(a = e.getElementById(i))) return n; if (a.id === i) return k.call(n, a), n } else if (f && (a = f.getElementById(i)) && I.contains(e, a) && a.id === i) return k.call(n, a), n } else { if (u[2]) return k.apply(n, e.getElementsByTagName(t)), n; if ((i = u[3]) && e.getElementsByClassName) return k.apply(n, e.getElementsByClassName(i)), n } if (!(h[t + " "] || d && d.test(t))) { if (c = t, f = e, 1 === p && (x.test(t) || m.test(t))) { (f = H.test(t) && U(e.parentNode) || e) == e && le.scope || ((s = e.getAttribute("id")) ? s = ce.escapeSelector(s) : e.setAttribute("id", s = S)), o = (l = Y(t)).length; while (o--) l[o] = (s ? "#" + s : ":scope") + " " + Q(l[o]); c = l.join(",") } try { return k.apply(n, f.querySelectorAll(c)), n } catch (e) { h(t, !0) } finally { s === S && e.removeAttribute("id") } } } return re(t.replace(ve, "$1"), e, n, r) } function W() { var r = []; return function e(t, n) { return r.push(t + " ") > b.cacheLength && delete e[r.shift()], e[t + " "] = n } } function F(e) { return e[S] = !0, e } function $(e) { var t = T.createElement("fieldset"); try { return !!e(t) } catch (e) { return !1 } finally { t.parentNode && t.parentNode.removeChild(t), t = null } } function B(t) { return function (e) { return fe(e, "input") && e.type === t } } function _(t) { return function (e) { return (fe(e, "input") || fe(e, "button")) && e.type === t } } function z(t) { return function (e) { return "form" in e ? e.parentNode && !1 === e.disabled ? "label" in e ? "label" in e.parentNode ? e.parentNode.disabled === t : e.disabled === t : e.isDisabled === t || e.isDisabled !== !t && R(e) === t : e.disabled === t : "label" in e && e.disabled === t } } function X(a) { return F(function (o) { return o = +o, F(function (e, t) { var n, r = a([], e.length, o), i = r.length; while (i--) e[n = r[i]] && (e[n] = !(t[n] = e[n])) }) }) } function U(e) { return e && "undefined" != typeof e.getElementsByTagName && e } function V(e) { var t, n = e ? e.ownerDocument || e : ye; return n != T && 9 === n.nodeType && n.documentElement && (r = (T = n).documentElement, C = !ce.isXMLDoc(T), i = r.matches || r.webkitMatchesSelector || r.msMatchesSelector, r.msMatchesSelector && ye != T && (t = T.defaultView) && t.top !== t && t.addEventListener("unload", M), le.getById = $(function (e) { return r.appendChild(e).id = ce.expando, !T.getElementsByName || !T.getElementsByName(ce.expando).length }), le.disconnectedMatch = $(function (e) { return i.call(e, "*") }), le.scope = $(function () { return T.querySelectorAll(":scope") }), le.cssHas = $(function () { try { return T.querySelector(":has(*,:jqfake)"), !1 } catch (e) { return !0 } }), le.getById ? (b.filter.ID = function (e) { var t = e.replace(O, P); return function (e) { return e.getAttribute("id") === t } }, b.find.ID = function (e, t) { if ("undefined" != typeof t.getElementById && C) { var n = t.getElementById(e); return n ? [n] : [] } }) : (b.filter.ID = function (e) { var n = e.replace(O, P); return function (e) { var t = "undefined" != typeof e.getAttributeNode && e.getAttributeNode("id"); return t && t.value === n } }, b.find.ID = function (e, t) { if ("undefined" != typeof t.getElementById && C) { var n, r, i, o = t.getElementById(e); if (o) { if ((n = o.getAttributeNode("id")) && n.value === e) return [o]; i = t.getElementsByName(e), r = 0; while (o = i[r++]) if ((n = o.getAttributeNode("id")) && n.value === e) return [o] } return [] } }), b.find.TAG = function (e, t) { return "undefined" != typeof t.getElementsByTagName ? t.getElementsByTagName(e) : t.querySelectorAll(e) }, b.find.CLASS = function (e, t) { if ("undefined" != typeof t.getElementsByClassName && C) return t.getElementsByClassName(e) }, d = [], $(function (e) { var t; r.appendChild(e).innerHTML = "", e.querySelectorAll("[selected]").length || d.push("\\[" + ge + "*(?:value|" + f + ")"), e.querySelectorAll("[id~=" + S + "-]").length || d.push("~="), e.querySelectorAll("a#" + S + "+*").length || d.push(".#.+[+~]"), e.querySelectorAll(":checked").length || d.push(":checked"), (t = T.createElement("input")).setAttribute("type", "hidden"), e.appendChild(t).setAttribute("name", "D"), r.appendChild(e).disabled = !0, 2 !== e.querySelectorAll(":disabled").length && d.push(":enabled", ":disabled"), (t = T.createElement("input")).setAttribute("name", ""), e.appendChild(t), e.querySelectorAll("[name='']").length || d.push("\\[" + ge + "*name" + ge + "*=" + ge + "*(?:''|\"\")") }), le.cssHas || d.push(":has"), d = d.length && new RegExp(d.join("|")), l = function (e, t) { if (e === t) return a = !0, 0; var n = !e.compareDocumentPosition - !t.compareDocumentPosition; return n || (1 & (n = (e.ownerDocument || e) == (t.ownerDocument || t) ? e.compareDocumentPosition(t) : 1) || !le.sortDetached && t.compareDocumentPosition(e) === n ? e === T || e.ownerDocument == ye && I.contains(ye, e) ? -1 : t === T || t.ownerDocument == ye && I.contains(ye, t) ? 1 : o ? se.call(o, e) - se.call(o, t) : 0 : 4 & n ? -1 : 1) }), T } for (e in I.matches = function (e, t) { return I(e, null, null, t) }, I.matchesSelector = function (e, t) { if (V(e), C && !h[t + " "] && (!d || !d.test(t))) try { var n = i.call(e, t); if (n || le.disconnectedMatch || e.document && 11 !== e.document.nodeType) return n } catch (e) { h(t, !0) } return 0 < I(t, T, null, [e]).length }, I.contains = function (e, t) { return (e.ownerDocument || e) != T && V(e), ce.contains(e, t) }, I.attr = function (e, t) { (e.ownerDocument || e) != T && V(e); var n = b.attrHandle[t.toLowerCase()], r = n && ue.call(b.attrHandle, t.toLowerCase()) ? n(e, t, !C) : void 0; return void 0 !== r ? r : e.getAttribute(t) }, I.error = function (e) { throw new Error("Syntax error, unrecognized expression: " + e) }, ce.uniqueSort = function (e) { var t, n = [], r = 0, i = 0; if (a = !le.sortStable, o = !le.sortStable && ae.call(e, 0), de.call(e, l), a) { while (t = e[i++]) t === e[i] && (r = n.push(i)); while (r--) he.call(e, n[r], 1) } return o = null, e }, ce.fn.uniqueSort = function () { return this.pushStack(ce.uniqueSort(ae.apply(this))) }, (b = ce.expr = { cacheLength: 50, createPseudo: F, match: D, attrHandle: {}, find: {}, relative: { ">": { dir: "parentNode", first: !0 }, " ": { dir: "parentNode" }, "+": { dir: "previousSibling", first: !0 }, "~": { dir: "previousSibling" } }, preFilter: { ATTR: function (e) { return e[1] = e[1].replace(O, P), e[3] = (e[3] || e[4] || e[5] || "").replace(O, P), "~=" === e[2] && (e[3] = " " + e[3] + " "), e.slice(0, 4) }, CHILD: function (e) { return e[1] = e[1].toLowerCase(), "nth" === e[1].slice(0, 3) ? (e[3] || I.error(e[0]), e[4] = +(e[4] ? e[5] + (e[6] || 1) : 2 * ("even" === e[3] || "odd" === e[3])), e[5] = +(e[7] + e[8] || "odd" === e[3])) : e[3] && I.error(e[0]), e }, PSEUDO: function (e) { var t, n = !e[6] && e[2]; return D.CHILD.test(e[0]) ? null : (e[3] ? e[2] = e[4] || e[5] || "" : n && j.test(n) && (t = Y(n, !0)) && (t = n.indexOf(")", n.length - t) - n.length) && (e[0] = e[0].slice(0, t), e[2] = n.slice(0, t)), e.slice(0, 3)) } }, filter: { TAG: function (e) { var t = e.replace(O, P).toLowerCase(); return "*" === e ? function () { return !0 } : function (e) { return fe(e, t) } }, CLASS: function (e) { var t = s[e + " "]; return t || (t = new RegExp("(^|" + ge + ")" + e + "(" + ge + "|$)")) && s(e, function (e) { return t.test("string" == typeof e.className && e.className || "undefined" != typeof e.getAttribute && e.getAttribute("class") || "") }) }, ATTR: function (n, r, i) { return function (e) { var t = I.attr(e, n); return null == t ? "!=" === r : !r || (t += "", "=" === r ? t === i : "!=" === r ? t !== i : "^=" === r ? i && 0 === t.indexOf(i) : "*=" === r ? i && -1 < t.indexOf(i) : "$=" === r ? i && t.slice(-i.length) === i : "~=" === r ? -1 < (" " + t.replace(v, " ") + " ").indexOf(i) : "|=" === r && (t === i || t.slice(0, i.length + 1) === i + "-")) } }, CHILD: function (d, e, t, h, g) { var v = "nth" !== d.slice(0, 3), y = "last" !== d.slice(-4), m = "of-type" === e; return 1 === h && 0 === g ? function (e) { return !!e.parentNode } : function (e, t, n) { var r, i, o, a, s, u = v !== y ? "nextSibling" : "previousSibling", l = e.parentNode, c = m && e.nodeName.toLowerCase(), f = !n && !m, p = !1; if (l) { if (v) { while (u) { o = e; while (o = o[u]) if (m ? fe(o, c) : 1 === o.nodeType) return !1; s = u = "only" === d && !s && "nextSibling" } return !0 } if (s = [y ? l.firstChild : l.lastChild], y && f) { p = (a = (r = (i = l[S] || (l[S] = {}))[d] || [])[0] === E && r[1]) && r[2], o = a && l.childNodes[a]; while (o = ++a && o && o[u] || (p = a = 0) || s.pop()) if (1 === o.nodeType && ++p && o === e) { i[d] = [E, a, p]; break } } else if (f && (p = a = (r = (i = e[S] || (e[S] = {}))[d] || [])[0] === E && r[1]), !1 === p) while (o = ++a && o && o[u] || (p = a = 0) || s.pop()) if ((m ? fe(o, c) : 1 === o.nodeType) && ++p && (f && ((i = o[S] || (o[S] = {}))[d] = [E, p]), o === e)) break; return (p -= g) === h || p % h == 0 && 0 <= p / h } } }, PSEUDO: function (e, o) { var t, a = b.pseudos[e] || b.setFilters[e.toLowerCase()] || I.error("unsupported pseudo: " + e); return a[S] ? a(o) : 1 < a.length ? (t = [e, e, "", o], b.setFilters.hasOwnProperty(e.toLowerCase()) ? F(function (e, t) { var n, r = a(e, o), i = r.length; while (i--) e[n = se.call(e, r[i])] = !(t[n] = r[i]) }) : function (e) { return a(e, 0, t) }) : a } }, pseudos: { not: F(function (e) { var r = [], i = [], s = ne(e.replace(ve, "$1")); return s[S] ? F(function (e, t, n, r) { var i, o = s(e, null, r, []), a = e.length; while (a--) (i = o[a]) && (e[a] = !(t[a] = i)) }) : function (e, t, n) { return r[0] = e, s(r, null, n, i), r[0] = null, !i.pop() } }), has: F(function (t) { return function (e) { return 0 < I(t, e).length } }), contains: F(function (t) { return t = t.replace(O, P), function (e) { return -1 < (e.textContent || ce.text(e)).indexOf(t) } }), lang: F(function (n) { return A.test(n || "") || I.error("unsupported lang: " + n), n = n.replace(O, P).toLowerCase(), function (e) { var t; do { if (t = C ? e.lang : e.getAttribute("xml:lang") || e.getAttribute("lang")) return (t = t.toLowerCase()) === n || 0 === t.indexOf(n + "-") } while ((e = e.parentNode) && 1 === e.nodeType); return !1 } }), target: function (e) { var t = ie.location && ie.location.hash; return t && t.slice(1) === e.id }, root: function (e) { return e === r }, focus: function (e) { return e === function () { try { return T.activeElement } catch (e) { } }() && T.hasFocus() && !!(e.type || e.href || ~e.tabIndex) }, enabled: z(!1), disabled: z(!0), checked: function (e) { return fe(e, "input") && !!e.checked || fe(e, "option") && !!e.selected }, selected: function (e) { return e.parentNode && e.parentNode.selectedIndex, !0 === e.selected }, empty: function (e) { for (e = e.firstChild; e; e = e.nextSibling)if (e.nodeType < 6) return !1; return !0 }, parent: function (e) { return !b.pseudos.empty(e) }, header: function (e) { return q.test(e.nodeName) }, input: function (e) { return N.test(e.nodeName) }, button: function (e) { return fe(e, "input") && "button" === e.type || fe(e, "button") }, text: function (e) { var t; return fe(e, "input") && "text" === e.type && (null == (t = e.getAttribute("type")) || "text" === t.toLowerCase()) }, first: X(function () { return [0] }), last: X(function (e, t) { return [t - 1] }), eq: X(function (e, t, n) { return [n < 0 ? n + t : n] }), even: X(function (e, t) { for (var n = 0; n < t; n += 2)e.push(n); return e }), odd: X(function (e, t) { for (var n = 1; n < t; n += 2)e.push(n); return e }), lt: X(function (e, t, n) { var r; for (r = n < 0 ? n + t : t < n ? t : n; 0 <= --r;)e.push(r); return e }), gt: X(function (e, t, n) { for (var r = n < 0 ? n + t : n; ++r < t;)e.push(r); return e }) } }).pseudos.nth = b.pseudos.eq, { radio: !0, checkbox: !0, file: !0, password: !0, image: !0 }) b.pseudos[e] = B(e); for (e in { submit: !0, reset: !0 }) b.pseudos[e] = _(e); function G() { } function Y(e, t) { var n, r, i, o, a, s, u, l = c[e + " "]; if (l) return t ? 0 : l.slice(0); a = e, s = [], u = b.preFilter; while (a) { for (o in n && !(r = y.exec(a)) || (r && (a = a.slice(r[0].length) || a), s.push(i = [])), n = !1, (r = m.exec(a)) && (n = r.shift(), i.push({ value: n, type: r[0].replace(ve, " ") }), a = a.slice(n.length)), b.filter) !(r = D[o].exec(a)) || u[o] && !(r = u[o](r)) || (n = r.shift(), i.push({ value: n, type: o, matches: r }), a = a.slice(n.length)); if (!n) break } return t ? a.length : a ? I.error(e) : c(e, s).slice(0) } function Q(e) { for (var t = 0, n = e.length, r = ""; t < n; t++)r += e[t].value; return r } function J(a, e, t) { var s = e.dir, u = e.next, l = u || s, c = t && "parentNode" === l, f = n++; return e.first ? function (e, t, n) { while (e = e[s]) if (1 === e.nodeType || c) return a(e, t, n); return !1 } : function (e, t, n) { var r, i, o = [E, f]; if (n) { while (e = e[s]) if ((1 === e.nodeType || c) && a(e, t, n)) return !0 } else while (e = e[s]) if (1 === e.nodeType || c) if (i = e[S] || (e[S] = {}), u && fe(e, u)) e = e[s] || e; else { if ((r = i[l]) && r[0] === E && r[1] === f) return o[2] = r[2]; if ((i[l] = o)[2] = a(e, t, n)) return !0 } return !1 } } function K(i) { return 1 < i.length ? function (e, t, n) { var r = i.length; while (r--) if (!i[r](e, t, n)) return !1; return !0 } : i[0] } function Z(e, t, n, r, i) { for (var o, a = [], s = 0, u = e.length, l = null != t; s < u; s++)(o = e[s]) && (n && !n(o, r, i) || (a.push(o), l && t.push(s))); return a } function ee(d, h, g, v, y, e) { return v && !v[S] && (v = ee(v)), y && !y[S] && (y = ee(y, e)), F(function (e, t, n, r) { var i, o, a, s, u = [], l = [], c = t.length, f = e || function (e, t, n) { for (var r = 0, i = t.length; r < i; r++)I(e, t[r], n); return n }(h || "*", n.nodeType ? [n] : n, []), p = !d || !e && h ? f : Z(f, u, d, n, r); if (g ? g(p, s = y || (e ? d : c || v) ? [] : t, n, r) : s = p, v) { i = Z(s, l), v(i, [], n, r), o = i.length; while (o--) (a = i[o]) && (s[l[o]] = !(p[l[o]] = a)) } if (e) { if (y || d) { if (y) { i = [], o = s.length; while (o--) (a = s[o]) && i.push(p[o] = a); y(null, s = [], i, r) } o = s.length; while (o--) (a = s[o]) && -1 < (i = y ? se.call(e, a) : u[o]) && (e[i] = !(t[i] = a)) } } else s = Z(s === t ? s.splice(c, s.length) : s), y ? y(null, t, s, r) : k.apply(t, s) }) } function te(e) { for (var i, t, n, r = e.length, o = b.relative[e[0].type], a = o || b.relative[" "], s = o ? 1 : 0, u = J(function (e) { return e === i }, a, !0), l = J(function (e) { return -1 < se.call(i, e) }, a, !0), c = [function (e, t, n) { var r = !o && (n || t != w) || ((i = t).nodeType ? u(e, t, n) : l(e, t, n)); return i = null, r }]; s < r; s++)if (t = b.relative[e[s].type]) c = [J(K(c), t)]; else { if ((t = b.filter[e[s].type].apply(null, e[s].matches))[S]) { for (n = ++s; n < r; n++)if (b.relative[e[n].type]) break; return ee(1 < s && K(c), 1 < s && Q(e.slice(0, s - 1).concat({ value: " " === e[s - 2].type ? "*" : "" })).replace(ve, "$1"), t, s < n && te(e.slice(s, n)), n < r && te(e = e.slice(n)), n < r && Q(e)) } c.push(t) } return K(c) } function ne(e, t) { var n, v, y, m, x, r, i = [], o = [], a = u[e + " "]; if (!a) { t || (t = Y(e)), n = t.length; while (n--) (a = te(t[n]))[S] ? i.push(a) : o.push(a); (a = u(e, (v = o, m = 0 < (y = i).length, x = 0 < v.length, r = function (e, t, n, r, i) { var o, a, s, u = 0, l = "0", c = e && [], f = [], p = w, d = e || x && b.find.TAG("*", i), h = E += null == p ? 1 : Math.random() || .1, g = d.length; for (i && (w = t == T || t || i); l !== g && null != (o = d[l]); l++) { if (x && o) { a = 0, t || o.ownerDocument == T || (V(o), n = !C); while (s = v[a++]) if (s(o, t || T, n)) { k.call(r, o); break } i && (E = h) } m && ((o = !s && o) && u--, e && c.push(o)) } if (u += l, m && l !== u) { a = 0; while (s = y[a++]) s(c, f, t, n); if (e) { if (0 < u) while (l--) c[l] || f[l] || (f[l] = pe.call(r)); f = Z(f) } k.apply(r, f), i && !e && 0 < f.length && 1 < u + y.length && ce.uniqueSort(r) } return i && (E = h, w = p), c }, m ? F(r) : r))).selector = e } return a } function re(e, t, n, r) { var i, o, a, s, u, l = "function" == typeof e && e, c = !r && Y(e = l.selector || e); if (n = n || [], 1 === c.length) { if (2 < (o = c[0] = c[0].slice(0)).length && "ID" === (a = o[0]).type && 9 === t.nodeType && C && b.relative[o[1].type]) { if (!(t = (b.find.ID(a.matches[0].replace(O, P), t) || [])[0])) return n; l && (t = t.parentNode), e = e.slice(o.shift().value.length) } i = D.needsContext.test(e) ? 0 : o.length; while (i--) { if (a = o[i], b.relative[s = a.type]) break; if ((u = b.find[s]) && (r = u(a.matches[0].replace(O, P), H.test(o[0].type) && U(t.parentNode) || t))) { if (o.splice(i, 1), !(e = r.length && Q(o))) return k.apply(n, r), n; break } } } return (l || ne(e, c))(r, t, !C, n, !t || H.test(e) && U(t.parentNode) || t), n } G.prototype = b.filters = b.pseudos, b.setFilters = new G, le.sortStable = S.split("").sort(l).join("") === S, V(), le.sortDetached = $(function (e) { return 1 & e.compareDocumentPosition(T.createElement("fieldset")) }), ce.find = I, ce.expr[":"] = ce.expr.pseudos, ce.unique = ce.uniqueSort, I.compile = ne, I.select = re, I.setDocument = V, I.tokenize = Y, I.escape = ce.escapeSelector, I.getText = ce.text, I.isXML = ce.isXMLDoc, I.selectors = ce.expr, I.support = ce.support, I.uniqueSort = ce.uniqueSort }(); var d = function (e, t, n) { var r = [], i = void 0 !== n; while ((e = e[t]) && 9 !== e.nodeType) if (1 === e.nodeType) { if (i && ce(e).is(n)) break; r.push(e) } return r }, h = function (e, t) { for (var n = []; e; e = e.nextSibling)1 === e.nodeType && e !== t && n.push(e); return n }, b = ce.expr.match.needsContext, w = /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i; function T(e, n, r) { return v(n) ? ce.grep(e, function (e, t) { return !!n.call(e, t, e) !== r }) : n.nodeType ? ce.grep(e, function (e) { return e === n !== r }) : "string" != typeof n ? ce.grep(e, function (e) { return -1 < se.call(n, e) !== r }) : ce.filter(n, e, r) } ce.filter = function (e, t, n) { var r = t[0]; return n && (e = ":not(" + e + ")"), 1 === t.length && 1 === r.nodeType ? ce.find.matchesSelector(r, e) ? [r] : [] : ce.find.matches(e, ce.grep(t, function (e) { return 1 === e.nodeType })) }, ce.fn.extend({ find: function (e) { var t, n, r = this.length, i = this; if ("string" != typeof e) return this.pushStack(ce(e).filter(function () { for (t = 0; t < r; t++)if (ce.contains(i[t], this)) return !0 })); for (n = this.pushStack([]), t = 0; t < r; t++)ce.find(e, i[t], n); return 1 < r ? ce.uniqueSort(n) : n }, filter: function (e) { return this.pushStack(T(this, e || [], !1)) }, not: function (e) { return this.pushStack(T(this, e || [], !0)) }, is: function (e) { return !!T(this, "string" == typeof e && b.test(e) ? ce(e) : e || [], !1).length } }); var k, S = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/; (ce.fn.init = function (e, t, n) { var r, i; if (!e) return this; if (n = n || k, "string" == typeof e) { if (!(r = "<" === e[0] && ">" === e[e.length - 1] && 3 <= e.length ? [null, e, null] : S.exec(e)) || !r[1] && t) return !t || t.jquery ? (t || n).find(e) : this.constructor(t).find(e); if (r[1]) { if (t = t instanceof ce ? t[0] : t, ce.merge(this, ce.parseHTML(r[1], t && t.nodeType ? t.ownerDocument || t : C, !0)), w.test(r[1]) && ce.isPlainObject(t)) for (r in t) v(this[r]) ? this[r](t[r]) : this.attr(r, t[r]); return this } return (i = C.getElementById(r[2])) && (this[0] = i, this.length = 1), this } return e.nodeType ? (this[0] = e, this.length = 1, this) : v(e) ? void 0 !== n.ready ? n.ready(e) : e(ce) : ce.makeArray(e, this) }).prototype = ce.fn, k = ce(C); var E = /^(?:parents|prev(?:Until|All))/, j = { children: !0, contents: !0, next: !0, prev: !0 }; function A(e, t) { while ((e = e[t]) && 1 !== e.nodeType); return e } ce.fn.extend({ has: function (e) { var t = ce(e, this), n = t.length; return this.filter(function () { for (var e = 0; e < n; e++)if (ce.contains(this, t[e])) return !0 }) }, closest: function (e, t) { var n, r = 0, i = this.length, o = [], a = "string" != typeof e && ce(e); if (!b.test(e)) for (; r < i; r++)for (n = this[r]; n && n !== t; n = n.parentNode)if (n.nodeType < 11 && (a ? -1 < a.index(n) : 1 === n.nodeType && ce.find.matchesSelector(n, e))) { o.push(n); break } return this.pushStack(1 < o.length ? ce.uniqueSort(o) : o) }, index: function (e) { return e ? "string" == typeof e ? se.call(ce(e), this[0]) : se.call(this, e.jquery ? e[0] : e) : this[0] && this[0].parentNode ? this.first().prevAll().length : -1 }, add: function (e, t) { return this.pushStack(ce.uniqueSort(ce.merge(this.get(), ce(e, t)))) }, addBack: function (e) { return this.add(null == e ? this.prevObject : this.prevObject.filter(e)) } }), ce.each({ parent: function (e) { var t = e.parentNode; return t && 11 !== t.nodeType ? t : null }, parents: function (e) { return d(e, "parentNode") }, parentsUntil: function (e, t, n) { return d(e, "parentNode", n) }, next: function (e) { return A(e, "nextSibling") }, prev: function (e) { return A(e, "previousSibling") }, nextAll: function (e) { return d(e, "nextSibling") }, prevAll: function (e) { return d(e, "previousSibling") }, nextUntil: function (e, t, n) { return d(e, "nextSibling", n) }, prevUntil: function (e, t, n) { return d(e, "previousSibling", n) }, siblings: function (e) { return h((e.parentNode || {}).firstChild, e) }, children: function (e) { return h(e.firstChild) }, contents: function (e) { return null != e.contentDocument && r(e.contentDocument) ? e.contentDocument : (fe(e, "template") && (e = e.content || e), ce.merge([], e.childNodes)) } }, function (r, i) { ce.fn[r] = function (e, t) { var n = ce.map(this, i, e); return "Until" !== r.slice(-5) && (t = e), t && "string" == typeof t && (n = ce.filter(t, n)), 1 < this.length && (j[r] || ce.uniqueSort(n), E.test(r) && n.reverse()), this.pushStack(n) } }); var D = /[^\x20\t\r\n\f]+/g; function N(e) { return e } function q(e) { throw e } function L(e, t, n, r) { var i; try { e && v(i = e.promise) ? i.call(e).done(t).fail(n) : e && v(i = e.then) ? i.call(e, t, n) : t.apply(void 0, [e].slice(r)) } catch (e) { n.apply(void 0, [e]) } } ce.Callbacks = function (r) { var e, n; r = "string" == typeof r ? (e = r, n = {}, ce.each(e.match(D) || [], function (e, t) { n[t] = !0 }), n) : ce.extend({}, r); var i, t, o, a, s = [], u = [], l = -1, c = function () { for (a = a || r.once, o = i = !0; u.length; l = -1) { t = u.shift(); while (++l < s.length) !1 === s[l].apply(t[0], t[1]) && r.stopOnFalse && (l = s.length, t = !1) } r.memory || (t = !1), i = !1, a && (s = t ? [] : "") }, f = { add: function () { return s && (t && !i && (l = s.length - 1, u.push(t)), function n(e) { ce.each(e, function (e, t) { v(t) ? r.unique && f.has(t) || s.push(t) : t && t.length && "string" !== x(t) && n(t) }) }(arguments), t && !i && c()), this }, remove: function () { return ce.each(arguments, function (e, t) { var n; while (-1 < (n = ce.inArray(t, s, n))) s.splice(n, 1), n <= l && l-- }), this }, has: function (e) { return e ? -1 < ce.inArray(e, s) : 0 < s.length }, empty: function () { return s && (s = []), this }, disable: function () { return a = u = [], s = t = "", this }, disabled: function () { return !s }, lock: function () { return a = u = [], t || i || (s = t = ""), this }, locked: function () { return !!a }, fireWith: function (e, t) { return a || (t = [e, (t = t || []).slice ? t.slice() : t], u.push(t), i || c()), this }, fire: function () { return f.fireWith(this, arguments), this }, fired: function () { return !!o } }; return f }, ce.extend({ Deferred: function (e) { var o = [["notify", "progress", ce.Callbacks("memory"), ce.Callbacks("memory"), 2], ["resolve", "done", ce.Callbacks("once memory"), ce.Callbacks("once memory"), 0, "resolved"], ["reject", "fail", ce.Callbacks("once memory"), ce.Callbacks("once memory"), 1, "rejected"]], i = "pending", a = { state: function () { return i }, always: function () { return s.done(arguments).fail(arguments), this }, "catch": function (e) { return a.then(null, e) }, pipe: function () { var i = arguments; return ce.Deferred(function (r) { ce.each(o, function (e, t) { var n = v(i[t[4]]) && i[t[4]]; s[t[1]](function () { var e = n && n.apply(this, arguments); e && v(e.promise) ? e.promise().progress(r.notify).done(r.resolve).fail(r.reject) : r[t[0] + "With"](this, n ? [e] : arguments) }) }), i = null }).promise() }, then: function (t, n, r) { var u = 0; function l(i, o, a, s) { return function () { var n = this, r = arguments, e = function () { var e, t; if (!(i < u)) { if ((e = a.apply(n, r)) === o.promise()) throw new TypeError("Thenable self-resolution"); t = e && ("object" == typeof e || "function" == typeof e) && e.then, v(t) ? s ? t.call(e, l(u, o, N, s), l(u, o, q, s)) : (u++, t.call(e, l(u, o, N, s), l(u, o, q, s), l(u, o, N, o.notifyWith))) : (a !== N && (n = void 0, r = [e]), (s || o.resolveWith)(n, r)) } }, t = s ? e : function () { try { e() } catch (e) { ce.Deferred.exceptionHook && ce.Deferred.exceptionHook(e, t.error), u <= i + 1 && (a !== q && (n = void 0, r = [e]), o.rejectWith(n, r)) } }; i ? t() : (ce.Deferred.getErrorHook ? t.error = ce.Deferred.getErrorHook() : ce.Deferred.getStackHook && (t.error = ce.Deferred.getStackHook()), ie.setTimeout(t)) } } return ce.Deferred(function (e) { o[0][3].add(l(0, e, v(r) ? r : N, e.notifyWith)), o[1][3].add(l(0, e, v(t) ? t : N)), o[2][3].add(l(0, e, v(n) ? n : q)) }).promise() }, promise: function (e) { return null != e ? ce.extend(e, a) : a } }, s = {}; return ce.each(o, function (e, t) { var n = t[2], r = t[5]; a[t[1]] = n.add, r && n.add(function () { i = r }, o[3 - e][2].disable, o[3 - e][3].disable, o[0][2].lock, o[0][3].lock), n.add(t[3].fire), s[t[0]] = function () { return s[t[0] + "With"](this === s ? void 0 : this, arguments), this }, s[t[0] + "With"] = n.fireWith }), a.promise(s), e && e.call(s, s), s }, when: function (e) { var n = arguments.length, t = n, r = Array(t), i = ae.call(arguments), o = ce.Deferred(), a = function (t) { return function (e) { r[t] = this, i[t] = 1 < arguments.length ? ae.call(arguments) : e, --n || o.resolveWith(r, i) } }; if (n <= 1 && (L(e, o.done(a(t)).resolve, o.reject, !n), "pending" === o.state() || v(i[t] && i[t].then))) return o.then(); while (t--) L(i[t], a(t), o.reject); return o.promise() } }); var H = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; ce.Deferred.exceptionHook = function (e, t) { ie.console && ie.console.warn && e && H.test(e.name) && ie.console.warn("jQuery.Deferred exception: " + e.message, e.stack, t) }, ce.readyException = function (e) { ie.setTimeout(function () { throw e }) }; var O = ce.Deferred(); function P() { C.removeEventListener("DOMContentLoaded", P), ie.removeEventListener("load", P), ce.ready() } ce.fn.ready = function (e) { return O.then(e)["catch"](function (e) { ce.readyException(e) }), this }, ce.extend({ isReady: !1, readyWait: 1, ready: function (e) { (!0 === e ? --ce.readyWait : ce.isReady) || (ce.isReady = !0) !== e && 0 < --ce.readyWait || O.resolveWith(C, [ce]) } }), ce.ready.then = O.then, "complete" === C.readyState || "loading" !== C.readyState && !C.documentElement.doScroll ? ie.setTimeout(ce.ready) : (C.addEventListener("DOMContentLoaded", P), ie.addEventListener("load", P)); var M = function (e, t, n, r, i, o, a) { var s = 0, u = e.length, l = null == n; if ("object" === x(n)) for (s in i = !0, n) M(e, t, s, n[s], !0, o, a); else if (void 0 !== r && (i = !0, v(r) || (a = !0), l && (a ? (t.call(e, r), t = null) : (l = t, t = function (e, t, n) { return l.call(ce(e), n) })), t)) for (; s < u; s++)t(e[s], n, a ? r : r.call(e[s], s, t(e[s], n))); return i ? e : l ? t.call(e) : u ? t(e[0], n) : o }, R = /^-ms-/, I = /-([a-z])/g; function W(e, t) { return t.toUpperCase() } function F(e) { return e.replace(R, "ms-").replace(I, W) } var $ = function (e) { return 1 === e.nodeType || 9 === e.nodeType || !+e.nodeType }; function B() { this.expando = ce.expando + B.uid++ } B.uid = 1, B.prototype = { cache: function (e) { var t = e[this.expando]; return t || (t = {}, $(e) && (e.nodeType ? e[this.expando] = t : Object.defineProperty(e, this.expando, { value: t, configurable: !0 }))), t }, set: function (e, t, n) { var r, i = this.cache(e); if ("string" == typeof t) i[F(t)] = n; else for (r in t) i[F(r)] = t[r]; return i }, get: function (e, t) { return void 0 === t ? this.cache(e) : e[this.expando] && e[this.expando][F(t)] }, access: function (e, t, n) { return void 0 === t || t && "string" == typeof t && void 0 === n ? this.get(e, t) : (this.set(e, t, n), void 0 !== n ? n : t) }, remove: function (e, t) { var n, r = e[this.expando]; if (void 0 !== r) { if (void 0 !== t) { n = (t = Array.isArray(t) ? t.map(F) : (t = F(t)) in r ? [t] : t.match(D) || []).length; while (n--) delete r[t[n]] } (void 0 === t || ce.isEmptyObject(r)) && (e.nodeType ? e[this.expando] = void 0 : delete e[this.expando]) } }, hasData: function (e) { var t = e[this.expando]; return void 0 !== t && !ce.isEmptyObject(t) } }; var _ = new B, z = new B, X = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, U = /[A-Z]/g; function V(e, t, n) { var r, i; if (void 0 === n && 1 === e.nodeType) if (r = "data-" + t.replace(U, "-$&").toLowerCase(), "string" == typeof (n = e.getAttribute(r))) { try { n = "true" === (i = n) || "false" !== i && ("null" === i ? null : i === +i + "" ? +i : X.test(i) ? JSON.parse(i) : i) } catch (e) { } z.set(e, t, n) } else n = void 0; return n } ce.extend({ hasData: function (e) { return z.hasData(e) || _.hasData(e) }, data: function (e, t, n) { return z.access(e, t, n) }, removeData: function (e, t) { z.remove(e, t) }, _data: function (e, t, n) { return _.access(e, t, n) }, _removeData: function (e, t) { _.remove(e, t) } }), ce.fn.extend({ data: function (n, e) { var t, r, i, o = this[0], a = o && o.attributes; if (void 0 === n) { if (this.length && (i = z.get(o), 1 === o.nodeType && !_.get(o, "hasDataAttrs"))) { t = a.length; while (t--) a[t] && 0 === (r = a[t].name).indexOf("data-") && (r = F(r.slice(5)), V(o, r, i[r])); _.set(o, "hasDataAttrs", !0) } return i } return "object" == typeof n ? this.each(function () { z.set(this, n) }) : M(this, function (e) { var t; if (o && void 0 === e) return void 0 !== (t = z.get(o, n)) ? t : void 0 !== (t = V(o, n)) ? t : void 0; this.each(function () { z.set(this, n, e) }) }, null, e, 1 < arguments.length, null, !0) }, removeData: function (e) { return this.each(function () { z.remove(this, e) }) } }), ce.extend({ queue: function (e, t, n) { var r; if (e) return t = (t || "fx") + "queue", r = _.get(e, t), n && (!r || Array.isArray(n) ? r = _.access(e, t, ce.makeArray(n)) : r.push(n)), r || [] }, dequeue: function (e, t) { t = t || "fx"; var n = ce.queue(e, t), r = n.length, i = n.shift(), o = ce._queueHooks(e, t); "inprogress" === i && (i = n.shift(), r--), i && ("fx" === t && n.unshift("inprogress"), delete o.stop, i.call(e, function () { ce.dequeue(e, t) }, o)), !r && o && o.empty.fire() }, _queueHooks: function (e, t) { var n = t + "queueHooks"; return _.get(e, n) || _.access(e, n, { empty: ce.Callbacks("once memory").add(function () { _.remove(e, [t + "queue", n]) }) }) } }), ce.fn.extend({ queue: function (t, n) { var e = 2; return "string" != typeof t && (n = t, t = "fx", e--), arguments.length < e ? ce.queue(this[0], t) : void 0 === n ? this : this.each(function () { var e = ce.queue(this, t, n); ce._queueHooks(this, t), "fx" === t && "inprogress" !== e[0] && ce.dequeue(this, t) }) }, dequeue: function (e) { return this.each(function () { ce.dequeue(this, e) }) }, clearQueue: function (e) { return this.queue(e || "fx", []) }, promise: function (e, t) { var n, r = 1, i = ce.Deferred(), o = this, a = this.length, s = function () { --r || i.resolveWith(o, [o]) }; "string" != typeof e && (t = e, e = void 0), e = e || "fx"; while (a--) (n = _.get(o[a], e + "queueHooks")) && n.empty && (r++, n.empty.add(s)); return s(), i.promise(t) } }); var G = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, Y = new RegExp("^(?:([+-])=|)(" + G + ")([a-z%]*)$", "i"), Q = ["Top", "Right", "Bottom", "Left"], J = C.documentElement, K = function (e) { return ce.contains(e.ownerDocument, e) }, Z = { composed: !0 }; J.getRootNode && (K = function (e) { return ce.contains(e.ownerDocument, e) || e.getRootNode(Z) === e.ownerDocument }); var ee = function (e, t) { return "none" === (e = t || e).style.display || "" === e.style.display && K(e) && "none" === ce.css(e, "display") }; function te(e, t, n, r) { var i, o, a = 20, s = r ? function () { return r.cur() } : function () { return ce.css(e, t, "") }, u = s(), l = n && n[3] || (ce.cssNumber[t] ? "" : "px"), c = e.nodeType && (ce.cssNumber[t] || "px" !== l && +u) && Y.exec(ce.css(e, t)); if (c && c[3] !== l) { u /= 2, l = l || c[3], c = +u || 1; while (a--) ce.style(e, t, c + l), (1 - o) * (1 - (o = s() / u || .5)) <= 0 && (a = 0), c /= o; c *= 2, ce.style(e, t, c + l), n = n || [] } return n && (c = +c || +u || 0, i = n[1] ? c + (n[1] + 1) * n[2] : +n[2], r && (r.unit = l, r.start = c, r.end = i)), i } var ne = {}; function re(e, t) { for (var n, r, i, o, a, s, u, l = [], c = 0, f = e.length; c < f; c++)(r = e[c]).style && (n = r.style.display, t ? ("none" === n && (l[c] = _.get(r, "display") || null, l[c] || (r.style.display = "")), "" === r.style.display && ee(r) && (l[c] = (u = a = o = void 0, a = (i = r).ownerDocument, s = i.nodeName, (u = ne[s]) || (o = a.body.appendChild(a.createElement(s)), u = ce.css(o, "display"), o.parentNode.removeChild(o), "none" === u && (u = "block"), ne[s] = u)))) : "none" !== n && (l[c] = "none", _.set(r, "display", n))); for (c = 0; c < f; c++)null != l[c] && (e[c].style.display = l[c]); return e } ce.fn.extend({ show: function () { return re(this, !0) }, hide: function () { return re(this) }, toggle: function (e) { return "boolean" == typeof e ? e ? this.show() : this.hide() : this.each(function () { ee(this) ? ce(this).show() : ce(this).hide() }) } }); var xe, be, we = /^(?:checkbox|radio)$/i, Te = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i, Ce = /^$|^module$|\/(?:java|ecma)script/i; xe = C.createDocumentFragment().appendChild(C.createElement("div")), (be = C.createElement("input")).setAttribute("type", "radio"), be.setAttribute("checked", "checked"), be.setAttribute("name", "t"), xe.appendChild(be), le.checkClone = xe.cloneNode(!0).cloneNode(!0).lastChild.checked, xe.innerHTML = "", le.noCloneChecked = !!xe.cloneNode(!0).lastChild.defaultValue, xe.innerHTML = "", le.option = !!xe.lastChild; var ke = { thead: [1, "", "
"], col: [2, "", "
"], tr: [2, "", "
"], td: [3, "", "
"], _default: [0, "", ""] }; function Se(e, t) { var n; return n = "undefined" != typeof e.getElementsByTagName ? e.getElementsByTagName(t || "*") : "undefined" != typeof e.querySelectorAll ? e.querySelectorAll(t || "*") : [], void 0 === t || t && fe(e, t) ? ce.merge([e], n) : n } function Ee(e, t) { for (var n = 0, r = e.length; n < r; n++)_.set(e[n], "globalEval", !t || _.get(t[n], "globalEval")) } ke.tbody = ke.tfoot = ke.colgroup = ke.caption = ke.thead, ke.th = ke.td, le.option || (ke.optgroup = ke.option = [1, ""]); var je = /<|&#?\w+;/; function Ae(e, t, n, r, i) { for (var o, a, s, u, l, c, f = t.createDocumentFragment(), p = [], d = 0, h = e.length; d < h; d++)if ((o = e[d]) || 0 === o) if ("object" === x(o)) ce.merge(p, o.nodeType ? [o] : o); else if (je.test(o)) { a = a || f.appendChild(t.createElement("div")), s = (Te.exec(o) || ["", ""])[1].toLowerCase(), u = ke[s] || ke._default, a.innerHTML = u[1] + ce.htmlPrefilter(o) + u[2], c = u[0]; while (c--) a = a.lastChild; ce.merge(p, a.childNodes), (a = f.firstChild).textContent = "" } else p.push(t.createTextNode(o)); f.textContent = "", d = 0; while (o = p[d++]) if (r && -1 < ce.inArray(o, r)) i && i.push(o); else if (l = K(o), a = Se(f.appendChild(o), "script"), l && Ee(a), n) { c = 0; while (o = a[c++]) Ce.test(o.type || "") && n.push(o) } return f } var De = /^([^.]*)(?:\.(.+)|)/; function Ne() { return !0 } function qe() { return !1 } function Le(e, t, n, r, i, o) { var a, s; if ("object" == typeof t) { for (s in "string" != typeof n && (r = r || n, n = void 0), t) Le(e, s, n, r, t[s], o); return e } if (null == r && null == i ? (i = n, r = n = void 0) : null == i && ("string" == typeof n ? (i = r, r = void 0) : (i = r, r = n, n = void 0)), !1 === i) i = qe; else if (!i) return e; return 1 === o && (a = i, (i = function (e) { return ce().off(e), a.apply(this, arguments) }).guid = a.guid || (a.guid = ce.guid++)), e.each(function () { ce.event.add(this, t, i, r, n) }) } function He(e, r, t) { t ? (_.set(e, r, !1), ce.event.add(e, r, { namespace: !1, handler: function (e) { var t, n = _.get(this, r); if (1 & e.isTrigger && this[r]) { if (n) (ce.event.special[r] || {}).delegateType && e.stopPropagation(); else if (n = ae.call(arguments), _.set(this, r, n), this[r](), t = _.get(this, r), _.set(this, r, !1), n !== t) return e.stopImmediatePropagation(), e.preventDefault(), t } else n && (_.set(this, r, ce.event.trigger(n[0], n.slice(1), this)), e.stopPropagation(), e.isImmediatePropagationStopped = Ne) } })) : void 0 === _.get(e, r) && ce.event.add(e, r, Ne) } ce.event = { global: {}, add: function (t, e, n, r, i) { var o, a, s, u, l, c, f, p, d, h, g, v = _.get(t); if ($(t)) { n.handler && (n = (o = n).handler, i = o.selector), i && ce.find.matchesSelector(J, i), n.guid || (n.guid = ce.guid++), (u = v.events) || (u = v.events = Object.create(null)), (a = v.handle) || (a = v.handle = function (e) { return "undefined" != typeof ce && ce.event.triggered !== e.type ? ce.event.dispatch.apply(t, arguments) : void 0 }), l = (e = (e || "").match(D) || [""]).length; while (l--) d = g = (s = De.exec(e[l]) || [])[1], h = (s[2] || "").split(".").sort(), d && (f = ce.event.special[d] || {}, d = (i ? f.delegateType : f.bindType) || d, f = ce.event.special[d] || {}, c = ce.extend({ type: d, origType: g, data: r, handler: n, guid: n.guid, selector: i, needsContext: i && ce.expr.match.needsContext.test(i), namespace: h.join(".") }, o), (p = u[d]) || ((p = u[d] = []).delegateCount = 0, f.setup && !1 !== f.setup.call(t, r, h, a) || t.addEventListener && t.addEventListener(d, a)), f.add && (f.add.call(t, c), c.handler.guid || (c.handler.guid = n.guid)), i ? p.splice(p.delegateCount++, 0, c) : p.push(c), ce.event.global[d] = !0) } }, remove: function (e, t, n, r, i) { var o, a, s, u, l, c, f, p, d, h, g, v = _.hasData(e) && _.get(e); if (v && (u = v.events)) { l = (t = (t || "").match(D) || [""]).length; while (l--) if (d = g = (s = De.exec(t[l]) || [])[1], h = (s[2] || "").split(".").sort(), d) { f = ce.event.special[d] || {}, p = u[d = (r ? f.delegateType : f.bindType) || d] || [], s = s[2] && new RegExp("(^|\\.)" + h.join("\\.(?:.*\\.|)") + "(\\.|$)"), a = o = p.length; while (o--) c = p[o], !i && g !== c.origType || n && n.guid !== c.guid || s && !s.test(c.namespace) || r && r !== c.selector && ("**" !== r || !c.selector) || (p.splice(o, 1), c.selector && p.delegateCount--, f.remove && f.remove.call(e, c)); a && !p.length && (f.teardown && !1 !== f.teardown.call(e, h, v.handle) || ce.removeEvent(e, d, v.handle), delete u[d]) } else for (d in u) ce.event.remove(e, d + t[l], n, r, !0); ce.isEmptyObject(u) && _.remove(e, "handle events") } }, dispatch: function (e) { var t, n, r, i, o, a, s = new Array(arguments.length), u = ce.event.fix(e), l = (_.get(this, "events") || Object.create(null))[u.type] || [], c = ce.event.special[u.type] || {}; for (s[0] = u, t = 1; t < arguments.length; t++)s[t] = arguments[t]; if (u.delegateTarget = this, !c.preDispatch || !1 !== c.preDispatch.call(this, u)) { a = ce.event.handlers.call(this, u, l), t = 0; while ((i = a[t++]) && !u.isPropagationStopped()) { u.currentTarget = i.elem, n = 0; while ((o = i.handlers[n++]) && !u.isImmediatePropagationStopped()) u.rnamespace && !1 !== o.namespace && !u.rnamespace.test(o.namespace) || (u.handleObj = o, u.data = o.data, void 0 !== (r = ((ce.event.special[o.origType] || {}).handle || o.handler).apply(i.elem, s)) && !1 === (u.result = r) && (u.preventDefault(), u.stopPropagation())) } return c.postDispatch && c.postDispatch.call(this, u), u.result } }, handlers: function (e, t) { var n, r, i, o, a, s = [], u = t.delegateCount, l = e.target; if (u && l.nodeType && !("click" === e.type && 1 <= e.button)) for (; l !== this; l = l.parentNode || this)if (1 === l.nodeType && ("click" !== e.type || !0 !== l.disabled)) { for (o = [], a = {}, n = 0; n < u; n++)void 0 === a[i = (r = t[n]).selector + " "] && (a[i] = r.needsContext ? -1 < ce(i, this).index(l) : ce.find(i, this, null, [l]).length), a[i] && o.push(r); o.length && s.push({ elem: l, handlers: o }) } return l = this, u < t.length && s.push({ elem: l, handlers: t.slice(u) }), s }, addProp: function (t, e) { Object.defineProperty(ce.Event.prototype, t, { enumerable: !0, configurable: !0, get: v(e) ? function () { if (this.originalEvent) return e(this.originalEvent) } : function () { if (this.originalEvent) return this.originalEvent[t] }, set: function (e) { Object.defineProperty(this, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) } }) }, fix: function (e) { return e[ce.expando] ? e : new ce.Event(e) }, special: { load: { noBubble: !0 }, click: { setup: function (e) { var t = this || e; return we.test(t.type) && t.click && fe(t, "input") && He(t, "click", !0), !1 }, trigger: function (e) { var t = this || e; return we.test(t.type) && t.click && fe(t, "input") && He(t, "click"), !0 }, _default: function (e) { var t = e.target; return we.test(t.type) && t.click && fe(t, "input") && _.get(t, "click") || fe(t, "a") } }, beforeunload: { postDispatch: function (e) { void 0 !== e.result && e.originalEvent && (e.originalEvent.returnValue = e.result) } } } }, ce.removeEvent = function (e, t, n) { e.removeEventListener && e.removeEventListener(t, n) }, ce.Event = function (e, t) { if (!(this instanceof ce.Event)) return new ce.Event(e, t); e && e.type ? (this.originalEvent = e, this.type = e.type, this.isDefaultPrevented = e.defaultPrevented || void 0 === e.defaultPrevented && !1 === e.returnValue ? Ne : qe, this.target = e.target && 3 === e.target.nodeType ? e.target.parentNode : e.target, this.currentTarget = e.currentTarget, this.relatedTarget = e.relatedTarget) : this.type = e, t && ce.extend(this, t), this.timeStamp = e && e.timeStamp || Date.now(), this[ce.expando] = !0 }, ce.Event.prototype = { constructor: ce.Event, isDefaultPrevented: qe, isPropagationStopped: qe, isImmediatePropagationStopped: qe, isSimulated: !1, preventDefault: function () { var e = this.originalEvent; this.isDefaultPrevented = Ne, e && !this.isSimulated && e.preventDefault() }, stopPropagation: function () { var e = this.originalEvent; this.isPropagationStopped = Ne, e && !this.isSimulated && e.stopPropagation() }, stopImmediatePropagation: function () { var e = this.originalEvent; this.isImmediatePropagationStopped = Ne, e && !this.isSimulated && e.stopImmediatePropagation(), this.stopPropagation() } }, ce.each({ altKey: !0, bubbles: !0, cancelable: !0, changedTouches: !0, ctrlKey: !0, detail: !0, eventPhase: !0, metaKey: !0, pageX: !0, pageY: !0, shiftKey: !0, view: !0, "char": !0, code: !0, charCode: !0, key: !0, keyCode: !0, button: !0, buttons: !0, clientX: !0, clientY: !0, offsetX: !0, offsetY: !0, pointerId: !0, pointerType: !0, screenX: !0, screenY: !0, targetTouches: !0, toElement: !0, touches: !0, which: !0 }, ce.event.addProp), ce.each({ focus: "focusin", blur: "focusout" }, function (r, i) { function o(e) { if (C.documentMode) { var t = _.get(this, "handle"), n = ce.event.fix(e); n.type = "focusin" === e.type ? "focus" : "blur", n.isSimulated = !0, t(e), n.target === n.currentTarget && t(n) } else ce.event.simulate(i, e.target, ce.event.fix(e)) } ce.event.special[r] = { setup: function () { var e; if (He(this, r, !0), !C.documentMode) return !1; (e = _.get(this, i)) || this.addEventListener(i, o), _.set(this, i, (e || 0) + 1) }, trigger: function () { return He(this, r), !0 }, teardown: function () { var e; if (!C.documentMode) return !1; (e = _.get(this, i) - 1) ? _.set(this, i, e) : (this.removeEventListener(i, o), _.remove(this, i)) }, _default: function (e) { return _.get(e.target, r) }, delegateType: i }, ce.event.special[i] = { setup: function () { var e = this.ownerDocument || this.document || this, t = C.documentMode ? this : e, n = _.get(t, i); n || (C.documentMode ? this.addEventListener(i, o) : e.addEventListener(r, o, !0)), _.set(t, i, (n || 0) + 1) }, teardown: function () { var e = this.ownerDocument || this.document || this, t = C.documentMode ? this : e, n = _.get(t, i) - 1; n ? _.set(t, i, n) : (C.documentMode ? this.removeEventListener(i, o) : e.removeEventListener(r, o, !0), _.remove(t, i)) } } }), ce.each({ mouseenter: "mouseover", mouseleave: "mouseout", pointerenter: "pointerover", pointerleave: "pointerout" }, function (e, i) { ce.event.special[e] = { delegateType: i, bindType: i, handle: function (e) { var t, n = e.relatedTarget, r = e.handleObj; return n && (n === this || ce.contains(this, n)) || (e.type = r.origType, t = r.handler.apply(this, arguments), e.type = i), t } } }), ce.fn.extend({ on: function (e, t, n, r) { return Le(this, e, t, n, r) }, one: function (e, t, n, r) { return Le(this, e, t, n, r, 1) }, off: function (e, t, n) { var r, i; if (e && e.preventDefault && e.handleObj) return r = e.handleObj, ce(e.delegateTarget).off(r.namespace ? r.origType + "." + r.namespace : r.origType, r.selector, r.handler), this; if ("object" == typeof e) { for (i in e) this.off(i, t, e[i]); return this } return !1 !== t && "function" != typeof t || (n = t, t = void 0), !1 === n && (n = qe), this.each(function () { ce.event.remove(this, e, n, t) }) } }); var Oe = /\s*$/g; function Re(e, t) { return fe(e, "table") && fe(11 !== t.nodeType ? t : t.firstChild, "tr") && ce(e).children("tbody")[0] || e } function Ie(e) { return e.type = (null !== e.getAttribute("type")) + "/" + e.type, e } function We(e) { return "true/" === (e.type || "").slice(0, 5) ? e.type = e.type.slice(5) : e.removeAttribute("type"), e } function Fe(e, t) { var n, r, i, o, a, s; if (1 === t.nodeType) { if (_.hasData(e) && (s = _.get(e).events)) for (i in _.remove(t, "handle events"), s) for (n = 0, r = s[i].length; n < r; n++)ce.event.add(t, i, s[i][n]); z.hasData(e) && (o = z.access(e), a = ce.extend({}, o), z.set(t, a)) } } function $e(n, r, i, o) { r = g(r); var e, t, a, s, u, l, c = 0, f = n.length, p = f - 1, d = r[0], h = v(d); if (h || 1 < f && "string" == typeof d && !le.checkClone && Pe.test(d)) return n.each(function (e) { var t = n.eq(e); h && (r[0] = d.call(this, e, t.html())), $e(t, r, i, o) }); if (f && (t = (e = Ae(r, n[0].ownerDocument, !1, n, o)).firstChild, 1 === e.childNodes.length && (e = t), t || o)) { for (s = (a = ce.map(Se(e, "script"), Ie)).length; c < f; c++)u = e, c !== p && (u = ce.clone(u, !0, !0), s && ce.merge(a, Se(u, "script"))), i.call(n[c], u, c); if (s) for (l = a[a.length - 1].ownerDocument, ce.map(a, We), c = 0; c < s; c++)u = a[c], Ce.test(u.type || "") && !_.access(u, "globalEval") && ce.contains(l, u) && (u.src && "module" !== (u.type || "").toLowerCase() ? ce._evalUrl && !u.noModule && ce._evalUrl(u.src, { nonce: u.nonce || u.getAttribute("nonce") }, l) : m(u.textContent.replace(Me, ""), u, l)) } return n } function Be(e, t, n) { for (var r, i = t ? ce.filter(t, e) : e, o = 0; null != (r = i[o]); o++)n || 1 !== r.nodeType || ce.cleanData(Se(r)), r.parentNode && (n && K(r) && Ee(Se(r, "script")), r.parentNode.removeChild(r)); return e } ce.extend({ htmlPrefilter: function (e) { return e }, clone: function (e, t, n) { var r, i, o, a, s, u, l, c = e.cloneNode(!0), f = K(e); if (!(le.noCloneChecked || 1 !== e.nodeType && 11 !== e.nodeType || ce.isXMLDoc(e))) for (a = Se(c), r = 0, i = (o = Se(e)).length; r < i; r++)s = o[r], u = a[r], void 0, "input" === (l = u.nodeName.toLowerCase()) && we.test(s.type) ? u.checked = s.checked : "input" !== l && "textarea" !== l || (u.defaultValue = s.defaultValue); if (t) if (n) for (o = o || Se(e), a = a || Se(c), r = 0, i = o.length; r < i; r++)Fe(o[r], a[r]); else Fe(e, c); return 0 < (a = Se(c, "script")).length && Ee(a, !f && Se(e, "script")), c }, cleanData: function (e) { for (var t, n, r, i = ce.event.special, o = 0; void 0 !== (n = e[o]); o++)if ($(n)) { if (t = n[_.expando]) { if (t.events) for (r in t.events) i[r] ? ce.event.remove(n, r) : ce.removeEvent(n, r, t.handle); n[_.expando] = void 0 } n[z.expando] && (n[z.expando] = void 0) } } }), ce.fn.extend({ detach: function (e) { return Be(this, e, !0) }, remove: function (e) { return Be(this, e) }, text: function (e) { return M(this, function (e) { return void 0 === e ? ce.text(this) : this.empty().each(function () { 1 !== this.nodeType && 11 !== this.nodeType && 9 !== this.nodeType || (this.textContent = e) }) }, null, e, arguments.length) }, append: function () { return $e(this, arguments, function (e) { 1 !== this.nodeType && 11 !== this.nodeType && 9 !== this.nodeType || Re(this, e).appendChild(e) }) }, prepend: function () { return $e(this, arguments, function (e) { if (1 === this.nodeType || 11 === this.nodeType || 9 === this.nodeType) { var t = Re(this, e); t.insertBefore(e, t.firstChild) } }) }, before: function () { return $e(this, arguments, function (e) { this.parentNode && this.parentNode.insertBefore(e, this) }) }, after: function () { return $e(this, arguments, function (e) { this.parentNode && this.parentNode.insertBefore(e, this.nextSibling) }) }, empty: function () { for (var e, t = 0; null != (e = this[t]); t++)1 === e.nodeType && (ce.cleanData(Se(e, !1)), e.textContent = ""); return this }, clone: function (e, t) { return e = null != e && e, t = null == t ? e : t, this.map(function () { return ce.clone(this, e, t) }) }, html: function (e) { return M(this, function (e) { var t = this[0] || {}, n = 0, r = this.length; if (void 0 === e && 1 === t.nodeType) return t.innerHTML; if ("string" == typeof e && !Oe.test(e) && !ke[(Te.exec(e) || ["", ""])[1].toLowerCase()]) { e = ce.htmlPrefilter(e); try { for (; n < r; n++)1 === (t = this[n] || {}).nodeType && (ce.cleanData(Se(t, !1)), t.innerHTML = e); t = 0 } catch (e) { } } t && this.empty().append(e) }, null, e, arguments.length) }, replaceWith: function () { var n = []; return $e(this, arguments, function (e) { var t = this.parentNode; ce.inArray(this, n) < 0 && (ce.cleanData(Se(this)), t && t.replaceChild(e, this)) }, n) } }), ce.each({ appendTo: "append", prependTo: "prepend", insertBefore: "before", insertAfter: "after", replaceAll: "replaceWith" }, function (e, a) { ce.fn[e] = function (e) { for (var t, n = [], r = ce(e), i = r.length - 1, o = 0; o <= i; o++)t = o === i ? this : this.clone(!0), ce(r[o])[a](t), s.apply(n, t.get()); return this.pushStack(n) } }); var _e = new RegExp("^(" + G + ")(?!px)[a-z%]+$", "i"), ze = /^--/, Xe = function (e) { var t = e.ownerDocument.defaultView; return t && t.opener || (t = ie), t.getComputedStyle(e) }, Ue = function (e, t, n) { var r, i, o = {}; for (i in t) o[i] = e.style[i], e.style[i] = t[i]; for (i in r = n.call(e), t) e.style[i] = o[i]; return r }, Ve = new RegExp(Q.join("|"), "i"); function Ge(e, t, n) { var r, i, o, a, s = ze.test(t), u = e.style; return (n = n || Xe(e)) && (a = n.getPropertyValue(t) || n[t], s && a && (a = a.replace(ve, "$1") || void 0), "" !== a || K(e) || (a = ce.style(e, t)), !le.pixelBoxStyles() && _e.test(a) && Ve.test(t) && (r = u.width, i = u.minWidth, o = u.maxWidth, u.minWidth = u.maxWidth = u.width = a, a = n.width, u.width = r, u.minWidth = i, u.maxWidth = o)), void 0 !== a ? a + "" : a } function Ye(e, t) { return { get: function () { if (!e()) return (this.get = t).apply(this, arguments); delete this.get } } } !function () { function e() { if (l) { u.style.cssText = "position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0", l.style.cssText = "position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%", J.appendChild(u).appendChild(l); var e = ie.getComputedStyle(l); n = "1%" !== e.top, s = 12 === t(e.marginLeft), l.style.right = "60%", o = 36 === t(e.right), r = 36 === t(e.width), l.style.position = "absolute", i = 12 === t(l.offsetWidth / 3), J.removeChild(u), l = null } } function t(e) { return Math.round(parseFloat(e)) } var n, r, i, o, a, s, u = C.createElement("div"), l = C.createElement("div"); l.style && (l.style.backgroundClip = "content-box", l.cloneNode(!0).style.backgroundClip = "", le.clearCloneStyle = "content-box" === l.style.backgroundClip, ce.extend(le, { boxSizingReliable: function () { return e(), r }, pixelBoxStyles: function () { return e(), o }, pixelPosition: function () { return e(), n }, reliableMarginLeft: function () { return e(), s }, scrollboxSize: function () { return e(), i }, reliableTrDimensions: function () { var e, t, n, r; return null == a && (e = C.createElement("table"), t = C.createElement("tr"), n = C.createElement("div"), e.style.cssText = "position:absolute;left:-11111px;border-collapse:separate", t.style.cssText = "box-sizing:content-box;border:1px solid", t.style.height = "1px", n.style.height = "9px", n.style.display = "block", J.appendChild(e).appendChild(t).appendChild(n), r = ie.getComputedStyle(t), a = parseInt(r.height, 10) + parseInt(r.borderTopWidth, 10) + parseInt(r.borderBottomWidth, 10) === t.offsetHeight, J.removeChild(e)), a } })) }(); var Qe = ["Webkit", "Moz", "ms"], Je = C.createElement("div").style, Ke = {}; function Ze(e) { var t = ce.cssProps[e] || Ke[e]; return t || (e in Je ? e : Ke[e] = function (e) { var t = e[0].toUpperCase() + e.slice(1), n = Qe.length; while (n--) if ((e = Qe[n] + t) in Je) return e }(e) || e) } var et = /^(none|table(?!-c[ea]).+)/, tt = { position: "absolute", visibility: "hidden", display: "block" }, nt = { letterSpacing: "0", fontWeight: "400" }; function rt(e, t, n) { var r = Y.exec(t); return r ? Math.max(0, r[2] - (n || 0)) + (r[3] || "px") : t } function it(e, t, n, r, i, o) { var a = "width" === t ? 1 : 0, s = 0, u = 0, l = 0; if (n === (r ? "border" : "content")) return 0; for (; a < 4; a += 2)"margin" === n && (l += ce.css(e, n + Q[a], !0, i)), r ? ("content" === n && (u -= ce.css(e, "padding" + Q[a], !0, i)), "margin" !== n && (u -= ce.css(e, "border" + Q[a] + "Width", !0, i))) : (u += ce.css(e, "padding" + Q[a], !0, i), "padding" !== n ? u += ce.css(e, "border" + Q[a] + "Width", !0, i) : s += ce.css(e, "border" + Q[a] + "Width", !0, i)); return !r && 0 <= o && (u += Math.max(0, Math.ceil(e["offset" + t[0].toUpperCase() + t.slice(1)] - o - u - s - .5)) || 0), u + l } function ot(e, t, n) { var r = Xe(e), i = (!le.boxSizingReliable() || n) && "border-box" === ce.css(e, "boxSizing", !1, r), o = i, a = Ge(e, t, r), s = "offset" + t[0].toUpperCase() + t.slice(1); if (_e.test(a)) { if (!n) return a; a = "auto" } return (!le.boxSizingReliable() && i || !le.reliableTrDimensions() && fe(e, "tr") || "auto" === a || !parseFloat(a) && "inline" === ce.css(e, "display", !1, r)) && e.getClientRects().length && (i = "border-box" === ce.css(e, "boxSizing", !1, r), (o = s in e) && (a = e[s])), (a = parseFloat(a) || 0) + it(e, t, n || (i ? "border" : "content"), o, r, a) + "px" } function at(e, t, n, r, i) { return new at.prototype.init(e, t, n, r, i) } ce.extend({ cssHooks: { opacity: { get: function (e, t) { if (t) { var n = Ge(e, "opacity"); return "" === n ? "1" : n } } } }, cssNumber: { animationIterationCount: !0, aspectRatio: !0, borderImageSlice: !0, columnCount: !0, flexGrow: !0, flexShrink: !0, fontWeight: !0, gridArea: !0, gridColumn: !0, gridColumnEnd: !0, gridColumnStart: !0, gridRow: !0, gridRowEnd: !0, gridRowStart: !0, lineHeight: !0, opacity: !0, order: !0, orphans: !0, scale: !0, widows: !0, zIndex: !0, zoom: !0, fillOpacity: !0, floodOpacity: !0, stopOpacity: !0, strokeMiterlimit: !0, strokeOpacity: !0 }, cssProps: {}, style: function (e, t, n, r) { if (e && 3 !== e.nodeType && 8 !== e.nodeType && e.style) { var i, o, a, s = F(t), u = ze.test(t), l = e.style; if (u || (t = Ze(s)), a = ce.cssHooks[t] || ce.cssHooks[s], void 0 === n) return a && "get" in a && void 0 !== (i = a.get(e, !1, r)) ? i : l[t]; "string" === (o = typeof n) && (i = Y.exec(n)) && i[1] && (n = te(e, t, i), o = "number"), null != n && n == n && ("number" !== o || u || (n += i && i[3] || (ce.cssNumber[s] ? "" : "px")), le.clearCloneStyle || "" !== n || 0 !== t.indexOf("background") || (l[t] = "inherit"), a && "set" in a && void 0 === (n = a.set(e, n, r)) || (u ? l.setProperty(t, n) : l[t] = n)) } }, css: function (e, t, n, r) { var i, o, a, s = F(t); return ze.test(t) || (t = Ze(s)), (a = ce.cssHooks[t] || ce.cssHooks[s]) && "get" in a && (i = a.get(e, !0, n)), void 0 === i && (i = Ge(e, t, r)), "normal" === i && t in nt && (i = nt[t]), "" === n || n ? (o = parseFloat(i), !0 === n || isFinite(o) ? o || 0 : i) : i } }), ce.each(["height", "width"], function (e, u) { ce.cssHooks[u] = { get: function (e, t, n) { if (t) return !et.test(ce.css(e, "display")) || e.getClientRects().length && e.getBoundingClientRect().width ? ot(e, u, n) : Ue(e, tt, function () { return ot(e, u, n) }) }, set: function (e, t, n) { var r, i = Xe(e), o = !le.scrollboxSize() && "absolute" === i.position, a = (o || n) && "border-box" === ce.css(e, "boxSizing", !1, i), s = n ? it(e, u, n, a, i) : 0; return a && o && (s -= Math.ceil(e["offset" + u[0].toUpperCase() + u.slice(1)] - parseFloat(i[u]) - it(e, u, "border", !1, i) - .5)), s && (r = Y.exec(t)) && "px" !== (r[3] || "px") && (e.style[u] = t, t = ce.css(e, u)), rt(0, t, s) } } }), ce.cssHooks.marginLeft = Ye(le.reliableMarginLeft, function (e, t) { if (t) return (parseFloat(Ge(e, "marginLeft")) || e.getBoundingClientRect().left - Ue(e, { marginLeft: 0 }, function () { return e.getBoundingClientRect().left })) + "px" }), ce.each({ margin: "", padding: "", border: "Width" }, function (i, o) { ce.cssHooks[i + o] = { expand: function (e) { for (var t = 0, n = {}, r = "string" == typeof e ? e.split(" ") : [e]; t < 4; t++)n[i + Q[t] + o] = r[t] || r[t - 2] || r[0]; return n } }, "margin" !== i && (ce.cssHooks[i + o].set = rt) }), ce.fn.extend({ css: function (e, t) { return M(this, function (e, t, n) { var r, i, o = {}, a = 0; if (Array.isArray(t)) { for (r = Xe(e), i = t.length; a < i; a++)o[t[a]] = ce.css(e, t[a], !1, r); return o } return void 0 !== n ? ce.style(e, t, n) : ce.css(e, t) }, e, t, 1 < arguments.length) } }), ((ce.Tween = at).prototype = { constructor: at, init: function (e, t, n, r, i, o) { this.elem = e, this.prop = n, this.easing = i || ce.easing._default, this.options = t, this.start = this.now = this.cur(), this.end = r, this.unit = o || (ce.cssNumber[n] ? "" : "px") }, cur: function () { var e = at.propHooks[this.prop]; return e && e.get ? e.get(this) : at.propHooks._default.get(this) }, run: function (e) { var t, n = at.propHooks[this.prop]; return this.options.duration ? this.pos = t = ce.easing[this.easing](e, this.options.duration * e, 0, 1, this.options.duration) : this.pos = t = e, this.now = (this.end - this.start) * t + this.start, this.options.step && this.options.step.call(this.elem, this.now, this), n && n.set ? n.set(this) : at.propHooks._default.set(this), this } }).init.prototype = at.prototype, (at.propHooks = { _default: { get: function (e) { var t; return 1 !== e.elem.nodeType || null != e.elem[e.prop] && null == e.elem.style[e.prop] ? e.elem[e.prop] : (t = ce.css(e.elem, e.prop, "")) && "auto" !== t ? t : 0 }, set: function (e) { ce.fx.step[e.prop] ? ce.fx.step[e.prop](e) : 1 !== e.elem.nodeType || !ce.cssHooks[e.prop] && null == e.elem.style[Ze(e.prop)] ? e.elem[e.prop] = e.now : ce.style(e.elem, e.prop, e.now + e.unit) } } }).scrollTop = at.propHooks.scrollLeft = { set: function (e) { e.elem.nodeType && e.elem.parentNode && (e.elem[e.prop] = e.now) } }, ce.easing = { linear: function (e) { return e }, swing: function (e) { return .5 - Math.cos(e * Math.PI) / 2 }, _default: "swing" }, ce.fx = at.prototype.init, ce.fx.step = {}; var st, ut, lt, ct, ft = /^(?:toggle|show|hide)$/, pt = /queueHooks$/; function dt() { ut && (!1 === C.hidden && ie.requestAnimationFrame ? ie.requestAnimationFrame(dt) : ie.setTimeout(dt, ce.fx.interval), ce.fx.tick()) } function ht() { return ie.setTimeout(function () { st = void 0 }), st = Date.now() } function gt(e, t) { var n, r = 0, i = { height: e }; for (t = t ? 1 : 0; r < 4; r += 2 - t)i["margin" + (n = Q[r])] = i["padding" + n] = e; return t && (i.opacity = i.width = e), i } function vt(e, t, n) { for (var r, i = (yt.tweeners[t] || []).concat(yt.tweeners["*"]), o = 0, a = i.length; o < a; o++)if (r = i[o].call(n, t, e)) return r } function yt(o, e, t) { var n, a, r = 0, i = yt.prefilters.length, s = ce.Deferred().always(function () { delete u.elem }), u = function () { if (a) return !1; for (var e = st || ht(), t = Math.max(0, l.startTime + l.duration - e), n = 1 - (t / l.duration || 0), r = 0, i = l.tweens.length; r < i; r++)l.tweens[r].run(n); return s.notifyWith(o, [l, n, t]), n < 1 && i ? t : (i || s.notifyWith(o, [l, 1, 0]), s.resolveWith(o, [l]), !1) }, l = s.promise({ elem: o, props: ce.extend({}, e), opts: ce.extend(!0, { specialEasing: {}, easing: ce.easing._default }, t), originalProperties: e, originalOptions: t, startTime: st || ht(), duration: t.duration, tweens: [], createTween: function (e, t) { var n = ce.Tween(o, l.opts, e, t, l.opts.specialEasing[e] || l.opts.easing); return l.tweens.push(n), n }, stop: function (e) { var t = 0, n = e ? l.tweens.length : 0; if (a) return this; for (a = !0; t < n; t++)l.tweens[t].run(1); return e ? (s.notifyWith(o, [l, 1, 0]), s.resolveWith(o, [l, e])) : s.rejectWith(o, [l, e]), this } }), c = l.props; for (!function (e, t) { var n, r, i, o, a; for (n in e) if (i = t[r = F(n)], o = e[n], Array.isArray(o) && (i = o[1], o = e[n] = o[0]), n !== r && (e[r] = o, delete e[n]), (a = ce.cssHooks[r]) && "expand" in a) for (n in o = a.expand(o), delete e[r], o) n in e || (e[n] = o[n], t[n] = i); else t[r] = i }(c, l.opts.specialEasing); r < i; r++)if (n = yt.prefilters[r].call(l, o, c, l.opts)) return v(n.stop) && (ce._queueHooks(l.elem, l.opts.queue).stop = n.stop.bind(n)), n; return ce.map(c, vt, l), v(l.opts.start) && l.opts.start.call(o, l), l.progress(l.opts.progress).done(l.opts.done, l.opts.complete).fail(l.opts.fail).always(l.opts.always), ce.fx.timer(ce.extend(u, { elem: o, anim: l, queue: l.opts.queue })), l } ce.Animation = ce.extend(yt, { tweeners: { "*": [function (e, t) { var n = this.createTween(e, t); return te(n.elem, e, Y.exec(t), n), n }] }, tweener: function (e, t) { v(e) ? (t = e, e = ["*"]) : e = e.match(D); for (var n, r = 0, i = e.length; r < i; r++)n = e[r], yt.tweeners[n] = yt.tweeners[n] || [], yt.tweeners[n].unshift(t) }, prefilters: [function (e, t, n) { var r, i, o, a, s, u, l, c, f = "width" in t || "height" in t, p = this, d = {}, h = e.style, g = e.nodeType && ee(e), v = _.get(e, "fxshow"); for (r in n.queue || (null == (a = ce._queueHooks(e, "fx")).unqueued && (a.unqueued = 0, s = a.empty.fire, a.empty.fire = function () { a.unqueued || s() }), a.unqueued++, p.always(function () { p.always(function () { a.unqueued--, ce.queue(e, "fx").length || a.empty.fire() }) })), t) if (i = t[r], ft.test(i)) { if (delete t[r], o = o || "toggle" === i, i === (g ? "hide" : "show")) { if ("show" !== i || !v || void 0 === v[r]) continue; g = !0 } d[r] = v && v[r] || ce.style(e, r) } if ((u = !ce.isEmptyObject(t)) || !ce.isEmptyObject(d)) for (r in f && 1 === e.nodeType && (n.overflow = [h.overflow, h.overflowX, h.overflowY], null == (l = v && v.display) && (l = _.get(e, "display")), "none" === (c = ce.css(e, "display")) && (l ? c = l : (re([e], !0), l = e.style.display || l, c = ce.css(e, "display"), re([e]))), ("inline" === c || "inline-block" === c && null != l) && "none" === ce.css(e, "float") && (u || (p.done(function () { h.display = l }), null == l && (c = h.display, l = "none" === c ? "" : c)), h.display = "inline-block")), n.overflow && (h.overflow = "hidden", p.always(function () { h.overflow = n.overflow[0], h.overflowX = n.overflow[1], h.overflowY = n.overflow[2] })), u = !1, d) u || (v ? "hidden" in v && (g = v.hidden) : v = _.access(e, "fxshow", { display: l }), o && (v.hidden = !g), g && re([e], !0), p.done(function () { for (r in g || re([e]), _.remove(e, "fxshow"), d) ce.style(e, r, d[r]) })), u = vt(g ? v[r] : 0, r, p), r in v || (v[r] = u.start, g && (u.end = u.start, u.start = 0)) }], prefilter: function (e, t) { t ? yt.prefilters.unshift(e) : yt.prefilters.push(e) } }), ce.speed = function (e, t, n) { var r = e && "object" == typeof e ? ce.extend({}, e) : { complete: n || !n && t || v(e) && e, duration: e, easing: n && t || t && !v(t) && t }; return ce.fx.off ? r.duration = 0 : "number" != typeof r.duration && (r.duration in ce.fx.speeds ? r.duration = ce.fx.speeds[r.duration] : r.duration = ce.fx.speeds._default), null != r.queue && !0 !== r.queue || (r.queue = "fx"), r.old = r.complete, r.complete = function () { v(r.old) && r.old.call(this), r.queue && ce.dequeue(this, r.queue) }, r }, ce.fn.extend({ fadeTo: function (e, t, n, r) { return this.filter(ee).css("opacity", 0).show().end().animate({ opacity: t }, e, n, r) }, animate: function (t, e, n, r) { var i = ce.isEmptyObject(t), o = ce.speed(e, n, r), a = function () { var e = yt(this, ce.extend({}, t), o); (i || _.get(this, "finish")) && e.stop(!0) }; return a.finish = a, i || !1 === o.queue ? this.each(a) : this.queue(o.queue, a) }, stop: function (i, e, o) { var a = function (e) { var t = e.stop; delete e.stop, t(o) }; return "string" != typeof i && (o = e, e = i, i = void 0), e && this.queue(i || "fx", []), this.each(function () { var e = !0, t = null != i && i + "queueHooks", n = ce.timers, r = _.get(this); if (t) r[t] && r[t].stop && a(r[t]); else for (t in r) r[t] && r[t].stop && pt.test(t) && a(r[t]); for (t = n.length; t--;)n[t].elem !== this || null != i && n[t].queue !== i || (n[t].anim.stop(o), e = !1, n.splice(t, 1)); !e && o || ce.dequeue(this, i) }) }, finish: function (a) { return !1 !== a && (a = a || "fx"), this.each(function () { var e, t = _.get(this), n = t[a + "queue"], r = t[a + "queueHooks"], i = ce.timers, o = n ? n.length : 0; for (t.finish = !0, ce.queue(this, a, []), r && r.stop && r.stop.call(this, !0), e = i.length; e--;)i[e].elem === this && i[e].queue === a && (i[e].anim.stop(!0), i.splice(e, 1)); for (e = 0; e < o; e++)n[e] && n[e].finish && n[e].finish.call(this); delete t.finish }) } }), ce.each(["toggle", "show", "hide"], function (e, r) { var i = ce.fn[r]; ce.fn[r] = function (e, t, n) { return null == e || "boolean" == typeof e ? i.apply(this, arguments) : this.animate(gt(r, !0), e, t, n) } }), ce.each({ slideDown: gt("show"), slideUp: gt("hide"), slideToggle: gt("toggle"), fadeIn: { opacity: "show" }, fadeOut: { opacity: "hide" }, fadeToggle: { opacity: "toggle" } }, function (e, r) { ce.fn[e] = function (e, t, n) { return this.animate(r, e, t, n) } }), ce.timers = [], ce.fx.tick = function () { var e, t = 0, n = ce.timers; for (st = Date.now(); t < n.length; t++)(e = n[t])() || n[t] !== e || n.splice(t--, 1); n.length || ce.fx.stop(), st = void 0 }, ce.fx.timer = function (e) { ce.timers.push(e), ce.fx.start() }, ce.fx.interval = 13, ce.fx.start = function () { ut || (ut = !0, dt()) }, ce.fx.stop = function () { ut = null }, ce.fx.speeds = { slow: 600, fast: 200, _default: 400 }, ce.fn.delay = function (r, e) { return r = ce.fx && ce.fx.speeds[r] || r, e = e || "fx", this.queue(e, function (e, t) { var n = ie.setTimeout(e, r); t.stop = function () { ie.clearTimeout(n) } }) }, lt = C.createElement("input"), ct = C.createElement("select").appendChild(C.createElement("option")), lt.type = "checkbox", le.checkOn = "" !== lt.value, le.optSelected = ct.selected, (lt = C.createElement("input")).value = "t", lt.type = "radio", le.radioValue = "t" === lt.value; var mt, xt = ce.expr.attrHandle; ce.fn.extend({ attr: function (e, t) { return M(this, ce.attr, e, t, 1 < arguments.length) }, removeAttr: function (e) { return this.each(function () { ce.removeAttr(this, e) }) } }), ce.extend({ attr: function (e, t, n) { var r, i, o = e.nodeType; if (3 !== o && 8 !== o && 2 !== o) return "undefined" == typeof e.getAttribute ? ce.prop(e, t, n) : (1 === o && ce.isXMLDoc(e) || (i = ce.attrHooks[t.toLowerCase()] || (ce.expr.match.bool.test(t) ? mt : void 0)), void 0 !== n ? null === n ? void ce.removeAttr(e, t) : i && "set" in i && void 0 !== (r = i.set(e, n, t)) ? r : (e.setAttribute(t, n + ""), n) : i && "get" in i && null !== (r = i.get(e, t)) ? r : null == (r = ce.find.attr(e, t)) ? void 0 : r) }, attrHooks: { type: { set: function (e, t) { if (!le.radioValue && "radio" === t && fe(e, "input")) { var n = e.value; return e.setAttribute("type", t), n && (e.value = n), t } } } }, removeAttr: function (e, t) { var n, r = 0, i = t && t.match(D); if (i && 1 === e.nodeType) while (n = i[r++]) e.removeAttribute(n) } }), mt = { set: function (e, t, n) { return !1 === t ? ce.removeAttr(e, n) : e.setAttribute(n, n), n } }, ce.each(ce.expr.match.bool.source.match(/\w+/g), function (e, t) { var a = xt[t] || ce.find.attr; xt[t] = function (e, t, n) { var r, i, o = t.toLowerCase(); return n || (i = xt[o], xt[o] = r, r = null != a(e, t, n) ? o : null, xt[o] = i), r } }); var bt = /^(?:input|select|textarea|button)$/i, wt = /^(?:a|area)$/i; function Tt(e) { return (e.match(D) || []).join(" ") } function Ct(e) { return e.getAttribute && e.getAttribute("class") || "" } function kt(e) { return Array.isArray(e) ? e : "string" == typeof e && e.match(D) || [] } ce.fn.extend({ prop: function (e, t) { return M(this, ce.prop, e, t, 1 < arguments.length) }, removeProp: function (e) { return this.each(function () { delete this[ce.propFix[e] || e] }) } }), ce.extend({ prop: function (e, t, n) { var r, i, o = e.nodeType; if (3 !== o && 8 !== o && 2 !== o) return 1 === o && ce.isXMLDoc(e) || (t = ce.propFix[t] || t, i = ce.propHooks[t]), void 0 !== n ? i && "set" in i && void 0 !== (r = i.set(e, n, t)) ? r : e[t] = n : i && "get" in i && null !== (r = i.get(e, t)) ? r : e[t] }, propHooks: { tabIndex: { get: function (e) { var t = ce.find.attr(e, "tabindex"); return t ? parseInt(t, 10) : bt.test(e.nodeName) || wt.test(e.nodeName) && e.href ? 0 : -1 } } }, propFix: { "for": "htmlFor", "class": "className" } }), le.optSelected || (ce.propHooks.selected = { get: function (e) { var t = e.parentNode; return t && t.parentNode && t.parentNode.selectedIndex, null }, set: function (e) { var t = e.parentNode; t && (t.selectedIndex, t.parentNode && t.parentNode.selectedIndex) } }), ce.each(["tabIndex", "readOnly", "maxLength", "cellSpacing", "cellPadding", "rowSpan", "colSpan", "useMap", "frameBorder", "contentEditable"], function () { ce.propFix[this.toLowerCase()] = this }), ce.fn.extend({ addClass: function (t) { var e, n, r, i, o, a; return v(t) ? this.each(function (e) { ce(this).addClass(t.call(this, e, Ct(this))) }) : (e = kt(t)).length ? this.each(function () { if (r = Ct(this), n = 1 === this.nodeType && " " + Tt(r) + " ") { for (o = 0; o < e.length; o++)i = e[o], n.indexOf(" " + i + " ") < 0 && (n += i + " "); a = Tt(n), r !== a && this.setAttribute("class", a) } }) : this }, removeClass: function (t) { var e, n, r, i, o, a; return v(t) ? this.each(function (e) { ce(this).removeClass(t.call(this, e, Ct(this))) }) : arguments.length ? (e = kt(t)).length ? this.each(function () { if (r = Ct(this), n = 1 === this.nodeType && " " + Tt(r) + " ") { for (o = 0; o < e.length; o++) { i = e[o]; while (-1 < n.indexOf(" " + i + " ")) n = n.replace(" " + i + " ", " ") } a = Tt(n), r !== a && this.setAttribute("class", a) } }) : this : this.attr("class", "") }, toggleClass: function (t, n) { var e, r, i, o, a = typeof t, s = "string" === a || Array.isArray(t); return v(t) ? this.each(function (e) { ce(this).toggleClass(t.call(this, e, Ct(this), n), n) }) : "boolean" == typeof n && s ? n ? this.addClass(t) : this.removeClass(t) : (e = kt(t), this.each(function () { if (s) for (o = ce(this), i = 0; i < e.length; i++)r = e[i], o.hasClass(r) ? o.removeClass(r) : o.addClass(r); else void 0 !== t && "boolean" !== a || ((r = Ct(this)) && _.set(this, "__className__", r), this.setAttribute && this.setAttribute("class", r || !1 === t ? "" : _.get(this, "__className__") || "")) })) }, hasClass: function (e) { var t, n, r = 0; t = " " + e + " "; while (n = this[r++]) if (1 === n.nodeType && -1 < (" " + Tt(Ct(n)) + " ").indexOf(t)) return !0; return !1 } }); var St = /\r/g; ce.fn.extend({ val: function (n) { var r, e, i, t = this[0]; return arguments.length ? (i = v(n), this.each(function (e) { var t; 1 === this.nodeType && (null == (t = i ? n.call(this, e, ce(this).val()) : n) ? t = "" : "number" == typeof t ? t += "" : Array.isArray(t) && (t = ce.map(t, function (e) { return null == e ? "" : e + "" })), (r = ce.valHooks[this.type] || ce.valHooks[this.nodeName.toLowerCase()]) && "set" in r && void 0 !== r.set(this, t, "value") || (this.value = t)) })) : t ? (r = ce.valHooks[t.type] || ce.valHooks[t.nodeName.toLowerCase()]) && "get" in r && void 0 !== (e = r.get(t, "value")) ? e : "string" == typeof (e = t.value) ? e.replace(St, "") : null == e ? "" : e : void 0 } }), ce.extend({ valHooks: { option: { get: function (e) { var t = ce.find.attr(e, "value"); return null != t ? t : Tt(ce.text(e)) } }, select: { get: function (e) { var t, n, r, i = e.options, o = e.selectedIndex, a = "select-one" === e.type, s = a ? null : [], u = a ? o + 1 : i.length; for (r = o < 0 ? u : a ? o : 0; r < u; r++)if (((n = i[r]).selected || r === o) && !n.disabled && (!n.parentNode.disabled || !fe(n.parentNode, "optgroup"))) { if (t = ce(n).val(), a) return t; s.push(t) } return s }, set: function (e, t) { var n, r, i = e.options, o = ce.makeArray(t), a = i.length; while (a--) ((r = i[a]).selected = -1 < ce.inArray(ce.valHooks.option.get(r), o)) && (n = !0); return n || (e.selectedIndex = -1), o } } } }), ce.each(["radio", "checkbox"], function () { ce.valHooks[this] = { set: function (e, t) { if (Array.isArray(t)) return e.checked = -1 < ce.inArray(ce(e).val(), t) } }, le.checkOn || (ce.valHooks[this].get = function (e) { return null === e.getAttribute("value") ? "on" : e.value }) }); var Et = ie.location, jt = { guid: Date.now() }, At = /\?/; ce.parseXML = function (e) { var t, n; if (!e || "string" != typeof e) return null; try { t = (new ie.DOMParser).parseFromString(e, "text/xml") } catch (e) { } return n = t && t.getElementsByTagName("parsererror")[0], t && !n || ce.error("Invalid XML: " + (n ? ce.map(n.childNodes, function (e) { return e.textContent }).join("\n") : e)), t }; var Dt = /^(?:focusinfocus|focusoutblur)$/, Nt = function (e) { e.stopPropagation() }; ce.extend(ce.event, { trigger: function (e, t, n, r) { var i, o, a, s, u, l, c, f, p = [n || C], d = ue.call(e, "type") ? e.type : e, h = ue.call(e, "namespace") ? e.namespace.split(".") : []; if (o = f = a = n = n || C, 3 !== n.nodeType && 8 !== n.nodeType && !Dt.test(d + ce.event.triggered) && (-1 < d.indexOf(".") && (d = (h = d.split(".")).shift(), h.sort()), u = d.indexOf(":") < 0 && "on" + d, (e = e[ce.expando] ? e : new ce.Event(d, "object" == typeof e && e)).isTrigger = r ? 2 : 3, e.namespace = h.join("."), e.rnamespace = e.namespace ? new RegExp("(^|\\.)" + h.join("\\.(?:.*\\.|)") + "(\\.|$)") : null, e.result = void 0, e.target || (e.target = n), t = null == t ? [e] : ce.makeArray(t, [e]), c = ce.event.special[d] || {}, r || !c.trigger || !1 !== c.trigger.apply(n, t))) { if (!r && !c.noBubble && !y(n)) { for (s = c.delegateType || d, Dt.test(s + d) || (o = o.parentNode); o; o = o.parentNode)p.push(o), a = o; a === (n.ownerDocument || C) && p.push(a.defaultView || a.parentWindow || ie) } i = 0; while ((o = p[i++]) && !e.isPropagationStopped()) f = o, e.type = 1 < i ? s : c.bindType || d, (l = (_.get(o, "events") || Object.create(null))[e.type] && _.get(o, "handle")) && l.apply(o, t), (l = u && o[u]) && l.apply && $(o) && (e.result = l.apply(o, t), !1 === e.result && e.preventDefault()); return e.type = d, r || e.isDefaultPrevented() || c._default && !1 !== c._default.apply(p.pop(), t) || !$(n) || u && v(n[d]) && !y(n) && ((a = n[u]) && (n[u] = null), ce.event.triggered = d, e.isPropagationStopped() && f.addEventListener(d, Nt), n[d](), e.isPropagationStopped() && f.removeEventListener(d, Nt), ce.event.triggered = void 0, a && (n[u] = a)), e.result } }, simulate: function (e, t, n) { var r = ce.extend(new ce.Event, n, { type: e, isSimulated: !0 }); ce.event.trigger(r, null, t) } }), ce.fn.extend({ trigger: function (e, t) { return this.each(function () { ce.event.trigger(e, t, this) }) }, triggerHandler: function (e, t) { var n = this[0]; if (n) return ce.event.trigger(e, t, n, !0) } }); var qt = /\[\]$/, Lt = /\r?\n/g, Ht = /^(?:submit|button|image|reset|file)$/i, Ot = /^(?:input|select|textarea|keygen)/i; function Pt(n, e, r, i) { var t; if (Array.isArray(e)) ce.each(e, function (e, t) { r || qt.test(n) ? i(n, t) : Pt(n + "[" + ("object" == typeof t && null != t ? e : "") + "]", t, r, i) }); else if (r || "object" !== x(e)) i(n, e); else for (t in e) Pt(n + "[" + t + "]", e[t], r, i) } ce.param = function (e, t) { var n, r = [], i = function (e, t) { var n = v(t) ? t() : t; r[r.length] = encodeURIComponent(e) + "=" + encodeURIComponent(null == n ? "" : n) }; if (null == e) return ""; if (Array.isArray(e) || e.jquery && !ce.isPlainObject(e)) ce.each(e, function () { i(this.name, this.value) }); else for (n in e) Pt(n, e[n], t, i); return r.join("&") }, ce.fn.extend({ serialize: function () { return ce.param(this.serializeArray()) }, serializeArray: function () { return this.map(function () { var e = ce.prop(this, "elements"); return e ? ce.makeArray(e) : this }).filter(function () { var e = this.type; return this.name && !ce(this).is(":disabled") && Ot.test(this.nodeName) && !Ht.test(e) && (this.checked || !we.test(e)) }).map(function (e, t) { var n = ce(this).val(); return null == n ? null : Array.isArray(n) ? ce.map(n, function (e) { return { name: t.name, value: e.replace(Lt, "\r\n") } }) : { name: t.name, value: n.replace(Lt, "\r\n") } }).get() } }); var Mt = /%20/g, Rt = /#.*$/, It = /([?&])_=[^&]*/, Wt = /^(.*?):[ \t]*([^\r\n]*)$/gm, Ft = /^(?:GET|HEAD)$/, $t = /^\/\//, Bt = {}, _t = {}, zt = "*/".concat("*"), Xt = C.createElement("a"); function Ut(o) { return function (e, t) { "string" != typeof e && (t = e, e = "*"); var n, r = 0, i = e.toLowerCase().match(D) || []; if (v(t)) while (n = i[r++]) "+" === n[0] ? (n = n.slice(1) || "*", (o[n] = o[n] || []).unshift(t)) : (o[n] = o[n] || []).push(t) } } function Vt(t, i, o, a) { var s = {}, u = t === _t; function l(e) { var r; return s[e] = !0, ce.each(t[e] || [], function (e, t) { var n = t(i, o, a); return "string" != typeof n || u || s[n] ? u ? !(r = n) : void 0 : (i.dataTypes.unshift(n), l(n), !1) }), r } return l(i.dataTypes[0]) || !s["*"] && l("*") } function Gt(e, t) { var n, r, i = ce.ajaxSettings.flatOptions || {}; for (n in t) void 0 !== t[n] && ((i[n] ? e : r || (r = {}))[n] = t[n]); return r && ce.extend(!0, e, r), e } Xt.href = Et.href, ce.extend({ active: 0, lastModified: {}, etag: {}, ajaxSettings: { url: Et.href, type: "GET", isLocal: /^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Et.protocol), global: !0, processData: !0, async: !0, contentType: "application/x-www-form-urlencoded; charset=UTF-8", accepts: { "*": zt, text: "text/plain", html: "text/html", xml: "application/xml, text/xml", json: "application/json, text/javascript" }, contents: { xml: /\bxml\b/, html: /\bhtml/, json: /\bjson\b/ }, responseFields: { xml: "responseXML", text: "responseText", json: "responseJSON" }, converters: { "* text": String, "text html": !0, "text json": JSON.parse, "text xml": ce.parseXML }, flatOptions: { url: !0, context: !0 } }, ajaxSetup: function (e, t) { return t ? Gt(Gt(e, ce.ajaxSettings), t) : Gt(ce.ajaxSettings, e) }, ajaxPrefilter: Ut(Bt), ajaxTransport: Ut(_t), ajax: function (e, t) { "object" == typeof e && (t = e, e = void 0), t = t || {}; var c, f, p, n, d, r, h, g, i, o, v = ce.ajaxSetup({}, t), y = v.context || v, m = v.context && (y.nodeType || y.jquery) ? ce(y) : ce.event, x = ce.Deferred(), b = ce.Callbacks("once memory"), w = v.statusCode || {}, a = {}, s = {}, u = "canceled", T = { readyState: 0, getResponseHeader: function (e) { var t; if (h) { if (!n) { n = {}; while (t = Wt.exec(p)) n[t[1].toLowerCase() + " "] = (n[t[1].toLowerCase() + " "] || []).concat(t[2]) } t = n[e.toLowerCase() + " "] } return null == t ? null : t.join(", ") }, getAllResponseHeaders: function () { return h ? p : null }, setRequestHeader: function (e, t) { return null == h && (e = s[e.toLowerCase()] = s[e.toLowerCase()] || e, a[e] = t), this }, overrideMimeType: function (e) { return null == h && (v.mimeType = e), this }, statusCode: function (e) { var t; if (e) if (h) T.always(e[T.status]); else for (t in e) w[t] = [w[t], e[t]]; return this }, abort: function (e) { var t = e || u; return c && c.abort(t), l(0, t), this } }; if (x.promise(T), v.url = ((e || v.url || Et.href) + "").replace($t, Et.protocol + "//"), v.type = t.method || t.type || v.method || v.type, v.dataTypes = (v.dataType || "*").toLowerCase().match(D) || [""], null == v.crossDomain) { r = C.createElement("a"); try { r.href = v.url, r.href = r.href, v.crossDomain = Xt.protocol + "//" + Xt.host != r.protocol + "//" + r.host } catch (e) { v.crossDomain = !0 } } if (v.data && v.processData && "string" != typeof v.data && (v.data = ce.param(v.data, v.traditional)), Vt(Bt, v, t, T), h) return T; for (i in (g = ce.event && v.global) && 0 == ce.active++ && ce.event.trigger("ajaxStart"), v.type = v.type.toUpperCase(), v.hasContent = !Ft.test(v.type), f = v.url.replace(Rt, ""), v.hasContent ? v.data && v.processData && 0 === (v.contentType || "").indexOf("application/x-www-form-urlencoded") && (v.data = v.data.replace(Mt, "+")) : (o = v.url.slice(f.length), v.data && (v.processData || "string" == typeof v.data) && (f += (At.test(f) ? "&" : "?") + v.data, delete v.data), !1 === v.cache && (f = f.replace(It, "$1"), o = (At.test(f) ? "&" : "?") + "_=" + jt.guid++ + o), v.url = f + o), v.ifModified && (ce.lastModified[f] && T.setRequestHeader("If-Modified-Since", ce.lastModified[f]), ce.etag[f] && T.setRequestHeader("If-None-Match", ce.etag[f])), (v.data && v.hasContent && !1 !== v.contentType || t.contentType) && T.setRequestHeader("Content-Type", v.contentType), T.setRequestHeader("Accept", v.dataTypes[0] && v.accepts[v.dataTypes[0]] ? v.accepts[v.dataTypes[0]] + ("*" !== v.dataTypes[0] ? ", " + zt + "; q=0.01" : "") : v.accepts["*"]), v.headers) T.setRequestHeader(i, v.headers[i]); if (v.beforeSend && (!1 === v.beforeSend.call(y, T, v) || h)) return T.abort(); if (u = "abort", b.add(v.complete), T.done(v.success), T.fail(v.error), c = Vt(_t, v, t, T)) { if (T.readyState = 1, g && m.trigger("ajaxSend", [T, v]), h) return T; v.async && 0 < v.timeout && (d = ie.setTimeout(function () { T.abort("timeout") }, v.timeout)); try { h = !1, c.send(a, l) } catch (e) { if (h) throw e; l(-1, e) } } else l(-1, "No Transport"); function l(e, t, n, r) { var i, o, a, s, u, l = t; h || (h = !0, d && ie.clearTimeout(d), c = void 0, p = r || "", T.readyState = 0 < e ? 4 : 0, i = 200 <= e && e < 300 || 304 === e, n && (s = function (e, t, n) { var r, i, o, a, s = e.contents, u = e.dataTypes; while ("*" === u[0]) u.shift(), void 0 === r && (r = e.mimeType || t.getResponseHeader("Content-Type")); if (r) for (i in s) if (s[i] && s[i].test(r)) { u.unshift(i); break } if (u[0] in n) o = u[0]; else { for (i in n) { if (!u[0] || e.converters[i + " " + u[0]]) { o = i; break } a || (a = i) } o = o || a } if (o) return o !== u[0] && u.unshift(o), n[o] }(v, T, n)), !i && -1 < ce.inArray("script", v.dataTypes) && ce.inArray("json", v.dataTypes) < 0 && (v.converters["text script"] = function () { }), s = function (e, t, n, r) { var i, o, a, s, u, l = {}, c = e.dataTypes.slice(); if (c[1]) for (a in e.converters) l[a.toLowerCase()] = e.converters[a]; o = c.shift(); while (o) if (e.responseFields[o] && (n[e.responseFields[o]] = t), !u && r && e.dataFilter && (t = e.dataFilter(t, e.dataType)), u = o, o = c.shift()) if ("*" === o) o = u; else if ("*" !== u && u !== o) { if (!(a = l[u + " " + o] || l["* " + o])) for (i in l) if ((s = i.split(" "))[1] === o && (a = l[u + " " + s[0]] || l["* " + s[0]])) { !0 === a ? a = l[i] : !0 !== l[i] && (o = s[0], c.unshift(s[1])); break } if (!0 !== a) if (a && e["throws"]) t = a(t); else try { t = a(t) } catch (e) { return { state: "parsererror", error: a ? e : "No conversion from " + u + " to " + o } } } return { state: "success", data: t } }(v, s, T, i), i ? (v.ifModified && ((u = T.getResponseHeader("Last-Modified")) && (ce.lastModified[f] = u), (u = T.getResponseHeader("etag")) && (ce.etag[f] = u)), 204 === e || "HEAD" === v.type ? l = "nocontent" : 304 === e ? l = "notmodified" : (l = s.state, o = s.data, i = !(a = s.error))) : (a = l, !e && l || (l = "error", e < 0 && (e = 0))), T.status = e, T.statusText = (t || l) + "", i ? x.resolveWith(y, [o, l, T]) : x.rejectWith(y, [T, l, a]), T.statusCode(w), w = void 0, g && m.trigger(i ? "ajaxSuccess" : "ajaxError", [T, v, i ? o : a]), b.fireWith(y, [T, l]), g && (m.trigger("ajaxComplete", [T, v]), --ce.active || ce.event.trigger("ajaxStop"))) } return T }, getJSON: function (e, t, n) { return ce.get(e, t, n, "json") }, getScript: function (e, t) { return ce.get(e, void 0, t, "script") } }), ce.each(["get", "post"], function (e, i) { ce[i] = function (e, t, n, r) { return v(t) && (r = r || n, n = t, t = void 0), ce.ajax(ce.extend({ url: e, type: i, dataType: r, data: t, success: n }, ce.isPlainObject(e) && e)) } }), ce.ajaxPrefilter(function (e) { var t; for (t in e.headers) "content-type" === t.toLowerCase() && (e.contentType = e.headers[t] || "") }), ce._evalUrl = function (e, t, n) { return ce.ajax({ url: e, type: "GET", dataType: "script", cache: !0, async: !1, global: !1, converters: { "text script": function () { } }, dataFilter: function (e) { ce.globalEval(e, t, n) } }) }, ce.fn.extend({ wrapAll: function (e) { var t; return this[0] && (v(e) && (e = e.call(this[0])), t = ce(e, this[0].ownerDocument).eq(0).clone(!0), this[0].parentNode && t.insertBefore(this[0]), t.map(function () { var e = this; while (e.firstElementChild) e = e.firstElementChild; return e }).append(this)), this }, wrapInner: function (n) { return v(n) ? this.each(function (e) { ce(this).wrapInner(n.call(this, e)) }) : this.each(function () { var e = ce(this), t = e.contents(); t.length ? t.wrapAll(n) : e.append(n) }) }, wrap: function (t) { var n = v(t); return this.each(function (e) { ce(this).wrapAll(n ? t.call(this, e) : t) }) }, unwrap: function (e) { return this.parent(e).not("body").each(function () { ce(this).replaceWith(this.childNodes) }), this } }), ce.expr.pseudos.hidden = function (e) { return !ce.expr.pseudos.visible(e) }, ce.expr.pseudos.visible = function (e) { return !!(e.offsetWidth || e.offsetHeight || e.getClientRects().length) }, ce.ajaxSettings.xhr = function () { try { return new ie.XMLHttpRequest } catch (e) { } }; var Yt = { 0: 200, 1223: 204 }, Qt = ce.ajaxSettings.xhr(); le.cors = !!Qt && "withCredentials" in Qt, le.ajax = Qt = !!Qt, ce.ajaxTransport(function (i) { var o, a; if (le.cors || Qt && !i.crossDomain) return { send: function (e, t) { var n, r = i.xhr(); if (r.open(i.type, i.url, i.async, i.username, i.password), i.xhrFields) for (n in i.xhrFields) r[n] = i.xhrFields[n]; for (n in i.mimeType && r.overrideMimeType && r.overrideMimeType(i.mimeType), i.crossDomain || e["X-Requested-With"] || (e["X-Requested-With"] = "XMLHttpRequest"), e) r.setRequestHeader(n, e[n]); o = function (e) { return function () { o && (o = a = r.onload = r.onerror = r.onabort = r.ontimeout = r.onreadystatechange = null, "abort" === e ? r.abort() : "error" === e ? "number" != typeof r.status ? t(0, "error") : t(r.status, r.statusText) : t(Yt[r.status] || r.status, r.statusText, "text" !== (r.responseType || "text") || "string" != typeof r.responseText ? { binary: r.response } : { text: r.responseText }, r.getAllResponseHeaders())) } }, r.onload = o(), a = r.onerror = r.ontimeout = o("error"), void 0 !== r.onabort ? r.onabort = a : r.onreadystatechange = function () { 4 === r.readyState && ie.setTimeout(function () { o && a() }) }, o = o("abort"); try { r.send(i.hasContent && i.data || null) } catch (e) { if (o) throw e } }, abort: function () { o && o() } } }), ce.ajaxPrefilter(function (e) { e.crossDomain && (e.contents.script = !1) }), ce.ajaxSetup({ accepts: { script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" }, contents: { script: /\b(?:java|ecma)script\b/ }, converters: { "text script": function (e) { return ce.globalEval(e), e } } }), ce.ajaxPrefilter("script", function (e) { void 0 === e.cache && (e.cache = !1), e.crossDomain && (e.type = "GET") }), ce.ajaxTransport("script", function (n) { var r, i; if (n.crossDomain || n.scriptAttrs) return { send: function (e, t) { r = ce(" + + + + +
+ + +
+ Tin tức + + + + Chi tiết tin tức +
+ + +
+ + +
+ + +
+ Mới tiên + + Google + News +
+ + +

+ + Apple ra mắt MacBook Neo giá rẻ chỉ 16,49 triệu đồng: Dùng chip iPhone, màn hình 13 inch, pin 16 giờ + +

+ + +
+
+ date + 15-02-2026, 10:35 pm +
+
+ author + Mai Nguyen +
+
+ + +
+

+ Thiết bị sử dụng chip A18 Pro, cho phép xử lý nhanh các tác vụ thường ngày như duyệt web, soạn thảo tài + liệu, xem video, chỉnh sửa ảnh hay chạy các ứng dụng AI +

+
    +
  1. + 1. MacBook có lượng vật liệu tái chế cao nhất của hãng +
  2. +
  3. + 2. MacBook có lượng vật liệu tái chế cao nhất của hãng +
  4. +
+
+ + +

+ + MacBook Neo trang bị màn hình Liquid Retina 13 inch với độ phân giải 2408 x 1506 pixel, độ sáng 500 nit và + hỗ trợ 1 tỷ màu + +

+ + +

+ Apple vừa giới thiệu MacBook Neo, mẫu MacBook hoàn toàn mới hướng tới người dùng phổ thông với thiết kế nhôm, + chip A18 Pro, màn hình Liquid Retina 13 inch thời lượng pin tới 16 giờ, giá tại Việt Nam từ 16,499 triệu đồng. +

+ + +
+ MacBook Neo +
+ + +
+

Thiết bị sử dụng chip A18 Pro, cho phép xử lý nhanh các tác vụ thường ngày như duyệt web, soạn thảo tài + liệu, xem video, chỉnh sửa ảnh hay chạy các ứng dụng AI. Apple cho biết MacBook Neo nhanh hơn tới 50% trong + các tác vụ phổ biến so với mẫu laptop PC bán chạy dùng Intel Core Ultra 5. Với các tác vụ AI chạy trực tiếp + trên thiết bị, hiệu năng có thể nhanh hơn tới 3 lần, trong khi chỉnh sửa ảnh nhanh hơn tới 2 lần.

+

Chip A18 Pro tích hợp GPU 5 lõi và Neural Engine 16 lõi, hỗ trợ các tính năng Apple Intelligence như tóm + tắt ghi chú, chỉnh sửa ảnh bằng công cụ Clean Up hoặc xử lý các tác vụ AI khác ngay trên thiết bị. MacBook + Neo cũng sử dụng thiết kế không quạt, giúp máy hoạt động hoàn toàn yên tĩnh.

+
+ + + MacBook Neo secondary + + +

+ Thời lượng pin đạt tối đa 16 giờ nhờ hiệu suất năng lượng của Apple silicon, cho phép người dùng làm việc hoặc + giải trí cả ngày mà không cần sạc. Máy cũng được trang bị camera FaceTime HD 1080p, hệ thống hai micro với + beamforming giảm tiếng ồn và loa kép hướng cạnh hỗ trợ Spatial Audio và Dolby Atmos. +

+ +
+ + + + +
+ + + + + +
+

Mọi người cũng tìm kiếm

+
+ hút mùi hafele + bếp từ cheft + nồi cơm điện Tiger + máy cạo râu Enchen + mũ bảo hiểm + nồi cơm điện Cuckoo + iPhone 15 + chảo chống dính Elmich + máy massage X5 Pro Omni + điều hòa Samsung + máy lọc nước Korihome Kangen K8 + tông đơ cắt tóc + điều hoà Hisense + Máy chạy bộ + MacBook Air M4 + điều hòa Nagakawa +
+
+ +
+ + + \ No newline at end of file diff --git a/export_to_html/cart.html b/export_to_html/cart.html new file mode 100644 index 0000000..6076841 --- /dev/null +++ b/export_to_html/cart.html @@ -0,0 +1,308 @@ + + + + + + + Giỏ hàng | MIQ + + + + + + + + + + +
+
+ Trang chủ + + + + Giỏ hàng +
+
+ + +
+
+
+ + +

Thông tin giỏ hàng

+ + +
+
+ Máy Quét Mã Vạch Symbol LS2208 +
+
+
+

Máy Quét Mã Vạch Symbol LS2208

+
+ +
+

1.850.000₫

+

2.000.000đ

+
+ +
+ +
+ 1 +
+ +
+ + +
+
+
+
+ + +
+
+
+ Máy in hóa đơn Zywell ZY901 +
+
+
+

Máy in hóa đơn Zywell ZY901

+
+ +
+

Liên hệ

+

 

+
+ +
+ +
+ 1 +
+ +
+ + +
+
+
+
+ +
+
+
+ • "Cam kết hàng chính hãng Liên hệ có giá tốt nhất Hỗ trợ kỹ thuật 24/7" +
+
+
+ + +

Thông tin nhận hàng

+ + +
+ +
+ + +
+ + +
+ +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + + + +
+
+ + + + +
+
+ +
+
+
+ + + + + +
+ Tổng tiền + 1.850.000₫ +
+ + +
+ + +
+

Phương thức thanh toán

+ +
+ + +
+ + + + + + +
+ +
+
+
+ + + + + + \ No newline at end of file diff --git a/export_to_html/category.html b/export_to_html/category.html new file mode 100644 index 0000000..3831544 --- /dev/null +++ b/export_to_html/category.html @@ -0,0 +1,511 @@ + + + + + + 329 Tá»§ lạnh - MIQ + + + + + + + +
+ +
+
+
+
+ + Hàng chính hãng - Bảo hành theo nhà sản xuất +
+
+ + Giao hàng toàn quốc +
+
+
+ + Hỗ trợ kỹ thuật 24/7 +
+
+
+ +
+
+ +
+ MIQ +
+ +
+ + Danh mục +
+ +
+
+ + Tìm kiếm sản phẩm... +
+ +
+ + Đăng nhập +
+ +
+ + Giỏ hàng +
+ +
+ + Showroom + +
+
+
+
+ +
+
+ Điện thoại + Laptop + Tivi - Màn hình + Máy lạnh + Tủ lạnh + Máy giặt + Bếp + Gia dụng + Đồng hồ - Camera +
+
+
+ + +
+
+ + +
+ Trang chủ + + 329 Tủ lạnh +
+ + +
+ +
+ +
+ +
+ +
+
+ Mẫu mới + Trả chậm 0% +
+
+ +
+ Sắm tết vô tư +
+
+

72.490.000 Ä‘

+ -20% +
+

7.490.000 Ä‘

+

Tủ lạnh Hitachi Inverter 540 lít Multi Door R-HW540RV(X)

+
+
+ + 5 · Đã bán 24,9k +
+
+ +
+
+
+
+ +
+
+ Trả chậm 0% +
+
+ +
+ Sắm tết vô tư +
+
+

72.490.000 Ä‘

+ -20% +
+

7.490.000 Ä‘

+

Tủ lạnh Hitachi Inverter 540 lít Multi Door R-HW540RV(X)

+
+
+ + 5 · Đã bán 24,9k +
+
+ +
+
+
+
+ +
+
+ Mẫu mới + Trả chậm 0% +
+
+ +
+ Sắm tết vô tư +
+
+

72.490.000 Ä‘

+ -20% +
+

7.490.000 Ä‘

+

Tủ lạnh Hitachi Inverter 540 lít Multi Door R-HW540RV(X)

+
+
+ + 5 · Đã bán 24,9k +
+
+ +
+
+
+
+ +
+
+ Trả chậm 0% +
+
+ +
+ Sắm tết vô tư +
+
+

72.490.000 Ä‘

+ -20% +
+

7.490.000 Ä‘

+

Tủ lạnh Hitachi Inverter 540 lít Multi Door R-HW540RV(X)

+
+
+ + 5 · Đã bán 24,9k +
+
+ +
+
+
+
+ +
+
+ Trả chậm 0% trả trước 0đ +
+
+ +
+ Sắm tết vô tư +
+
+

72.490.000 Ä‘

+ -20% +
+

7.490.000 Ä‘

+

Tủ lạnh Hitachi Inverter 540 lít Multi Door R-HW540RV(X)

+
+
+ + 5 · Đã bán 24,9k +
+
+ +
+
+
+
+
+
+ + +
+
+ +
+
+ +
+
+ + +
+ +
+ + + + + + + + + + +
+ +
+ Sắp xếp theo: + Nổi bật + · + Bán chạy + · + Giảm giá + · + Mới + · + Giá +
+ +
+ + +
+
+ Mẫu mới + Trả chậm 0% +
+ + Sắm tết vô tư +

7.490.000 Ä‘

+

7.490.000 Ä‘

-20%
+

Tủ lạnh Samsung Inverter 380 lít RT38T509TB1/SV

+
+
5 · Đã bán 24,9k
+
+
+ + Sắm tết vô tư +

7.490.000 Ä‘

+

7.490.000 Ä‘

-20%
+

Tủ lạnh Samsung Inverter 380 lít RT38T509TB1/SV

+
+
5 · Đã bán 24,9k
+
+
+
+ Mẫu mới + Trả chậm 0% +
+ + Sắm tết vô tư +

7.490.000 Ä‘

+

7.490.000 Ä‘

-20%
+

Tủ lạnh Samsung Inverter 380 lít RT38T509TB1/SV

+
+
5 · Đã bán 24,9k
+
+
+ + Sắm tết vô tư +

7.490.000 Ä‘

+

7.490.000 Ä‘

-20%
+

Tủ lạnh Samsung Inverter 380 lít RT38T509TB1/SV

+
+
5 · Đã bán 24,9k
+
+
+ + Sắm tết vô tư +

7.490.000 Ä‘

+

7.490.000 Ä‘

-20%
+

Tủ lạnh Samsung Inverter 380 lít RT38T509TB1/SV

+
+
5 · Đã bán 24,9k
+
+ + +
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh Hitachi Inverter 540 lít Multi Door

5 · Đã bán 24,9k
+
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh Hitachi Inverter 540 lít Multi Door

5 · Đã bán 24,9k
+
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh Hitachi Inverter 540 lít Multi Door

5 · Đã bán 24,9k
+
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh Hitachi Inverter 540 lít Multi Door

5 · Đã bán 24,9k
+
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh Hitachi Inverter 540 lít Multi Door

5 · Đã bán 24,9k
+ +
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh LG Inverter 394 lít GN-D392BLA

5 · Đã bán 24,9k
+
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh LG Inverter 394 lít GN-D392BLA

5 · Đã bán 24,9k
+
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh LG Inverter 394 lít GN-D392BLA

5 · Đã bán 24,9k
+
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh LG Inverter 394 lít GN-D392BLA

5 · Đã bán 24,9k
+
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh LG Inverter 394 lít GN-D392BLA

5 · Đã bán 24,9k
+ +
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh Toshiba Inverter 555 lít GR-RF669WI-PMV

5 · Đã bán 24,9k
+
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh Toshiba Inverter 555 lít GR-RF669WI-PMV

5 · Đã bán 24,9k
+
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh Toshiba Inverter 555 lít GR-RF669WI-PMV

5 · Đã bán 24,9k
+
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh Toshiba Inverter 555 lít GR-RF669WI-PMV

5 · Đã bán 24,9k
+
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh Toshiba Inverter 555 lít GR-RF669WI-PMV

5 · Đã bán 24,9k
+ +
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh Casper Inverter 236 lít RT-238VW

5 · Đã bán 24,9k
+
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh Casper Inverter 236 lít RT-238VW

5 · Đã bán 24,9k
+
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh Casper Inverter 236 lít RT-238VW

5 · Đã bán 24,9k
+
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh Casper Inverter 236 lít RT-238VW

5 · Đã bán 24,9k
+
Sắm tết vô tư

7.490.000 Ä‘

7.490.000 Ä‘

-20%

Tủ lạnh Casper Inverter 236 lít RT-238VW

5 · Đã bán 24,9k
+
+
+ + +
+ +
+

TIN KHUYẾN MẠI

+
+
+
+ +
+

Ngày hội ĐIỆN MÁY - SIÊU RẺ. Duy nhất 17h ngày 10/02 Cơ hội trúng Máy nước

+
+
+
+ +

Ngày hội ĐIỆN MÁY - SIÊU RẺ. Duy nhất 17h ngày 10/02 Cơ hội trúng Máy nước

+
+
+ +

Ngày hội ĐIỆN MÁY - SIÊU RẺ. Duy nhất 17h ngày 10/02 Cơ hội trúng Máy nước

+
+
+ +

Ngày hội ĐIỆN MÁY - SIÊU RẺ. Duy nhất 17h ngày 10/02 Cơ hội trúng Máy nước

+
+
+
+
+ +
+

VIDEO REVIEW

+
+
+ +
+
+
+
+ +
+
+

Duy nhất 17h ngày 10/02 Cơ hội trúng Máy nước

+
+
+
+ +
+
+

Duy nhất 17h ngày 10/02 Cơ hội trúng Máy nước

+
+
+
+
+ + +
+

Mọi người cũng tìm kiếm

+
+ tủ lạnh Samsungtủ lạnh LGtủ lạnh Hitachitủ lạnh Toshibatủ lạnh invertertủ lạnh side by sidetủ lạnh 4 cánhtủ lạnh minitủ lạnh Caspertủ lạnh 2 cánhtủ lạnh Sharptủ lạnh Panasonictủ lạnh Electroluxtủ lạnh Aquatủ lạnh 300 líttủ lạnh 400 líttủ lạnh 500 líttủ lạnh French doortủ lạnh multi doortủ lạnh giá rẻ +
+
+ +
+
+ + +
+
+
+

Tổng đài hỗ trợ

+
+

Gọi mua: 0971.49.0000 (8h00-18h00)

+

Hỗ trợ kỹ thuật : 0987.762.444 (8h00-18h00)

+

Góp ý - Khiếu nại: 082.418.4444 (8h00-18h00)

+
+
+
+

Về công ty

+
+

Giới thiệu về công ty

Tuyển dụng

Gửi góp ý, khiếu nại

Tìm siêu thị (215 shop)

+
+
+
+

Chính sách chung

+
+

Quy định truy cập Website

Chính sách bảo mật thông tin

Chính sách vận chuyển, giao nhận

Chính sách cho doanh nghiệp

Chính sách bảo hành, đổi trả

+
+
+
+

Kết nối với chúng tôi

+
+
+ + 12.8k lượt thích +
+
+ + 2.2T Đăng ký +
+
+ + Zalo MIQ +
+
+
+ Bộ Công Thương +
+
+
+
+

Bản quyền thuộc về MIQ Việt Nam

+
+
+ + + + diff --git a/export_to_html/home-article.html b/export_to_html/home-article.html new file mode 100644 index 0000000..7196f07 --- /dev/null +++ b/export_to_html/home-article.html @@ -0,0 +1,560 @@ + + + + + + + Tin Tức - MIQ Việt Nam + + + + + + + +
+
+
+ + + + + + Sản phẩm Chính hãng - xuất VAT đầy đủ + + + + + + + + + Giao nhanh - Miễn phí + +
+
+ + + + Hotline: 0967641641 +
+
+
+ + +
+
+ + MIQ + + + + + +
+
+
+ + + + + Tivi Samsung giá sốc +
+
+ + +
+ + + + + Đăng nhập +
+ + +
+ + + + + + Giỏ hàng +
+ + + +
+
+
+ + +
+
+ máy lạnh + tủ lạnh + máy lọc nước + quạt sưởi + nồi chiên + smart tivi + loa + nồi cơm điện + màn hình máy tính +
+
+ +
+ + +
+ Tin tức + + + + Chi tiết tin tức +
+ + +
+ + + + +
+ + +
+ + Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít + +

+ Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít và lì xì đến 1.780.000đ duy nhất tại MIQ +

+
+ + + + + 15/01/2026 +
+
+ +
+ + Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít + +

+ Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít và lì xì đến 1.780.000đ duy nhất tại MIQ +

+
+ + + + + 15/01/2026 +
+
+ +
+ + Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít + +

+ Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít và lì xì đến 1.780.000đ duy nhất tại MIQ +

+
+ + + + + 15/01/2026 +
+
+ +
+ + Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít + +

+ Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít và lì xì đến 1.780.000đ duy nhất tại MIQ +

+
+ + + + + 15/01/2026 +
+
+ +
+ + Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít + +

+ Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít và lì xì đến 1.780.000đ duy nhất tại MIQ +

+
+ + + + + 15/01/2026 +
+
+ +
+ + Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít + +

+ Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít và lì xì đến 1.780.000đ duy nhất tại MIQ +

+
+ + + + + 15/01/2026 +
+
+ +
+ + Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít + +

+ Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít và lì xì đến 1.780.000đ duy nhất tại MIQ +

+
+ + + + + 15/01/2026 +
+
+ +
+ + Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít + +

+ Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít và lì xì đến 1.780.000đ duy nhất tại MIQ +

+
+ + + + + 15/01/2026 +
+
+ +
+ + Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít + +

+ Tết 2026: Sắm máy lọc nước Korihome tặng ngay tủ lạnh Aqua 90 lít và lì xì đến 1.780.000đ duy nhất tại MIQ +

+
+ + + + + 15/01/2026 +
+
+ +
+ + +
+ 1 + 2 + 3 + ... + + + + + +
+
+ + + + + +
+

Mọi người cũng tìm kiếm

+
+ hút mùi hafele + bếp từ cheft + nồi cơm điện Tiger + máy cạo râu Enchen + mũ bảo hiểm + nồi cơm điện Cuckoo + iPhone 15 + chảo chống dính Elmich + máy massage X5 Pro Omni + điều hòa Samsung + máy lọc nước Korihome Kangen K8 + tông đơ cắt tóc + điều hoà Hisense + Máy chạy bộ + MacBook Air M4 + điều hòa Nagakawa +
+
+ +
+ + +
+
+ +
+

Tổng đài hỗ trợ

+
+

Gọi mua: 0971.49.0000 + (8h00-18h00) +

+

Hỗ trợ kỹ thuật: 0987.762.444 + (8h00-18h00) +

+

Góp ý - Khiếu nại: 082.418.4444 + (8h00-18h00) +

+
+
+ + +
+

Về công ty

+
+

Giới thiệu về công ty

+

Tuyển dụng

+

Gửi góp ý, khiếu nại

+

Tìm siêu thị (215 shop)

+
+
+ + +
+

Chính sách chung

+
+

Quy định truy cập Website

+

Chính sách bảo mật thông tin

+

Chính sách vận chuyển, giao nhận

+

Chính sách cho doanh nghiệp

+

Chính sách bảo hành, đổi trả

+
+
+ + +
+

Kết nối với chúng tôi

+
+
+ + + + 12.8k lượt thích +
+
+ + + + 2.2T Đăng ký +
+
+ + + + Zalo MIQ +
+
+ +
+ BCT Đã ĐK
+
+
+ + +
+

Bản quyền thuộc về MIQ Việt Nam

+
+
+ + + + \ No newline at end of file diff --git a/export_to_html/image/article_main_image.png b/export_to_html/image/article_main_image.png new file mode 100644 index 0000000..7bf0fc7 Binary files /dev/null and b/export_to_html/image/article_main_image.png differ diff --git a/export_to_html/image/article_thumb_1.png b/export_to_html/image/article_thumb_1.png new file mode 100644 index 0000000..8d0a56c Binary files /dev/null and b/export_to_html/image/article_thumb_1.png differ diff --git a/export_to_html/image/article_thumb_2.png b/export_to_html/image/article_thumb_2.png new file mode 100644 index 0000000..24021f7 Binary files /dev/null and b/export_to_html/image/article_thumb_2.png differ diff --git a/export_to_html/image/article_thumb_3.png b/export_to_html/image/article_thumb_3.png new file mode 100644 index 0000000..f241ba8 Binary files /dev/null and b/export_to_html/image/article_thumb_3.png differ diff --git a/export_to_html/image/banner_slider.png b/export_to_html/image/banner_slider.png new file mode 100644 index 0000000..69aaa78 Binary files /dev/null and b/export_to_html/image/banner_slider.png differ diff --git a/export_to_html/image/brand_aqua.png b/export_to_html/image/brand_aqua.png new file mode 100644 index 0000000..ba2703a Binary files /dev/null and b/export_to_html/image/brand_aqua.png differ diff --git a/export_to_html/image/brand_lg.png b/export_to_html/image/brand_lg.png new file mode 100644 index 0000000..60e2bdf Binary files /dev/null and b/export_to_html/image/brand_lg.png differ diff --git a/export_to_html/image/brand_samsung.png b/export_to_html/image/brand_samsung.png new file mode 100644 index 0000000..d8f7bb3 Binary files /dev/null and b/export_to_html/image/brand_samsung.png differ diff --git a/export_to_html/image/brand_sony.png b/export_to_html/image/brand_sony.png new file mode 100644 index 0000000..4181bef Binary files /dev/null and b/export_to_html/image/brand_sony.png differ diff --git a/export_to_html/image/brand_tcl.png b/export_to_html/image/brand_tcl.png new file mode 100644 index 0000000..286b6a2 Binary files /dev/null and b/export_to_html/image/brand_tcl.png differ diff --git a/export_to_html/image/brand_toshiba.png b/export_to_html/image/brand_toshiba.png new file mode 100644 index 0000000..b478f00 Binary files /dev/null and b/export_to_html/image/brand_toshiba.png differ diff --git a/export_to_html/image/bst_sale_bg.png b/export_to_html/image/bst_sale_bg.png new file mode 100644 index 0000000..fede8aa Binary files /dev/null and b/export_to_html/image/bst_sale_bg.png differ diff --git a/export_to_html/image/category_banner_dien_may.png b/export_to_html/image/category_banner_dien_may.png new file mode 100644 index 0000000..e9d50c0 Binary files /dev/null and b/export_to_html/image/category_banner_dien_may.png differ diff --git a/export_to_html/image/category_banner_dien_tu.png b/export_to_html/image/category_banner_dien_tu.png new file mode 100644 index 0000000..e9d50c0 Binary files /dev/null and b/export_to_html/image/category_banner_dien_tu.png differ diff --git a/export_to_html/image/category_banner_gia_dung.png b/export_to_html/image/category_banner_gia_dung.png new file mode 100644 index 0000000..e9d50c0 Binary files /dev/null and b/export_to_html/image/category_banner_gia_dung.png differ diff --git a/export_to_html/image/category_banner_sale_top.png b/export_to_html/image/category_banner_sale_top.png new file mode 100644 index 0000000..41246f6 Binary files /dev/null and b/export_to_html/image/category_banner_sale_top.png differ diff --git a/export_to_html/image/category_banner_vien_thong.png b/export_to_html/image/category_banner_vien_thong.png new file mode 100644 index 0000000..e9d50c0 Binary files /dev/null and b/export_to_html/image/category_banner_vien_thong.png differ diff --git a/export_to_html/image/category_qc_lg.png b/export_to_html/image/category_qc_lg.png new file mode 100644 index 0000000..54f87a6 Binary files /dev/null and b/export_to_html/image/category_qc_lg.png differ diff --git a/export_to_html/image/category_qc_samsung.png b/export_to_html/image/category_qc_samsung.png new file mode 100644 index 0000000..4226d25 Binary files /dev/null and b/export_to_html/image/category_qc_samsung.png differ diff --git a/export_to_html/image/category_sale_product_1.png b/export_to_html/image/category_sale_product_1.png new file mode 100644 index 0000000..2a1489f Binary files /dev/null and b/export_to_html/image/category_sale_product_1.png differ diff --git a/export_to_html/image/category_sale_product_2.png b/export_to_html/image/category_sale_product_2.png new file mode 100644 index 0000000..25087a2 Binary files /dev/null and b/export_to_html/image/category_sale_product_2.png differ diff --git a/export_to_html/image/category_sale_product_3.png b/export_to_html/image/category_sale_product_3.png new file mode 100644 index 0000000..7829c7c Binary files /dev/null and b/export_to_html/image/category_sale_product_3.png differ diff --git a/export_to_html/image/category_sale_product_4.png b/export_to_html/image/category_sale_product_4.png new file mode 100644 index 0000000..7d15bfe Binary files /dev/null and b/export_to_html/image/category_sale_product_4.png differ diff --git a/export_to_html/image/category_sale_product_5.png b/export_to_html/image/category_sale_product_5.png new file mode 100644 index 0000000..9329018 Binary files /dev/null and b/export_to_html/image/category_sale_product_5.png differ diff --git a/export_to_html/image/header_slogan_bg.png b/export_to_html/image/header_slogan_bg.png new file mode 100644 index 0000000..7bef7ff Binary files /dev/null and b/export_to_html/image/header_slogan_bg.png differ diff --git a/export_to_html/image/icon_arrow_next.png b/export_to_html/image/icon_arrow_next.png new file mode 100644 index 0000000..7247bb5 --- /dev/null +++ b/export_to_html/image/icon_arrow_next.png @@ -0,0 +1,3 @@ + + + diff --git a/export_to_html/image/icon_arrow_next.svg b/export_to_html/image/icon_arrow_next.svg new file mode 100644 index 0000000..7247bb5 --- /dev/null +++ b/export_to_html/image/icon_arrow_next.svg @@ -0,0 +1,3 @@ + + + diff --git a/export_to_html/image/icon_cart.png b/export_to_html/image/icon_cart.png new file mode 100644 index 0000000..7bcd7f6 --- /dev/null +++ b/export_to_html/image/icon_cart.png @@ -0,0 +1,3 @@ + + + diff --git a/export_to_html/image/icon_cart.svg b/export_to_html/image/icon_cart.svg new file mode 100644 index 0000000..83b0d01 --- /dev/null +++ b/export_to_html/image/icon_cart.svg @@ -0,0 +1,3 @@ + + + diff --git a/export_to_html/image/icon_cart_circle.png b/export_to_html/image/icon_cart_circle.png new file mode 100644 index 0000000..17b771b --- /dev/null +++ b/export_to_html/image/icon_cart_circle.png @@ -0,0 +1,3 @@ + + + diff --git a/export_to_html/image/icon_cart_circle.svg b/export_to_html/image/icon_cart_circle.svg new file mode 100644 index 0000000..d65fe92 --- /dev/null +++ b/export_to_html/image/icon_cart_circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/export_to_html/image/icon_category_item.png b/export_to_html/image/icon_category_item.png new file mode 100644 index 0000000..55f7872 Binary files /dev/null and b/export_to_html/image/icon_category_item.png differ diff --git a/export_to_html/image/icon_certificate.png b/export_to_html/image/icon_certificate.png new file mode 100644 index 0000000..4b49738 --- /dev/null +++ b/export_to_html/image/icon_certificate.png @@ -0,0 +1,5 @@ + + + + + diff --git a/export_to_html/image/icon_certificate.svg b/export_to_html/image/icon_certificate.svg new file mode 100644 index 0000000..d346103 --- /dev/null +++ b/export_to_html/image/icon_certificate.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/export_to_html/image/icon_chevron_right_menu.svg b/export_to_html/image/icon_chevron_right_menu.svg new file mode 100644 index 0000000..1e2881c --- /dev/null +++ b/export_to_html/image/icon_chevron_right_menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/export_to_html/image/icon_close.png b/export_to_html/image/icon_close.png new file mode 100644 index 0000000..508c400 --- /dev/null +++ b/export_to_html/image/icon_close.png @@ -0,0 +1,5 @@ + + + + + diff --git a/export_to_html/image/icon_close.svg b/export_to_html/image/icon_close.svg new file mode 100644 index 0000000..bc126a1 --- /dev/null +++ b/export_to_html/image/icon_close.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/export_to_html/image/icon_customer_support.png b/export_to_html/image/icon_customer_support.png new file mode 100644 index 0000000..a8dec73 --- /dev/null +++ b/export_to_html/image/icon_customer_support.png @@ -0,0 +1,7 @@ + + + + + + + diff --git a/export_to_html/image/icon_customer_support.svg b/export_to_html/image/icon_customer_support.svg new file mode 100644 index 0000000..3a41dc0 --- /dev/null +++ b/export_to_html/image/icon_customer_support.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/export_to_html/image/icon_delivery_truck.png b/export_to_html/image/icon_delivery_truck.png new file mode 100644 index 0000000..b6f95b1 --- /dev/null +++ b/export_to_html/image/icon_delivery_truck.png @@ -0,0 +1,6 @@ + + + + + + diff --git a/export_to_html/image/icon_delivery_truck.svg b/export_to_html/image/icon_delivery_truck.svg new file mode 100644 index 0000000..eb56bbe --- /dev/null +++ b/export_to_html/image/icon_delivery_truck.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/export_to_html/image/icon_double_next.png b/export_to_html/image/icon_double_next.png new file mode 100644 index 0000000..47c67aa --- /dev/null +++ b/export_to_html/image/icon_double_next.png @@ -0,0 +1,5 @@ + + + + + diff --git a/export_to_html/image/icon_double_next.svg b/export_to_html/image/icon_double_next.svg new file mode 100644 index 0000000..8b37661 --- /dev/null +++ b/export_to_html/image/icon_double_next.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/export_to_html/image/icon_double_next_small.png b/export_to_html/image/icon_double_next_small.png new file mode 100644 index 0000000..23f9baf --- /dev/null +++ b/export_to_html/image/icon_double_next_small.png @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/export_to_html/image/icon_double_next_small.svg b/export_to_html/image/icon_double_next_small.svg new file mode 100644 index 0000000..6121caf --- /dev/null +++ b/export_to_html/image/icon_double_next_small.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/export_to_html/image/icon_facebook.png b/export_to_html/image/icon_facebook.png new file mode 100644 index 0000000..7c04d11 --- /dev/null +++ b/export_to_html/image/icon_facebook.png @@ -0,0 +1,3 @@ + + + diff --git a/export_to_html/image/icon_facebook.svg b/export_to_html/image/icon_facebook.svg new file mode 100644 index 0000000..f5d21c0 --- /dev/null +++ b/export_to_html/image/icon_facebook.svg @@ -0,0 +1,4 @@ + + + + diff --git a/export_to_html/image/icon_fire.png b/export_to_html/image/icon_fire.png new file mode 100644 index 0000000..5db316a --- /dev/null +++ b/export_to_html/image/icon_fire.png @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/export_to_html/image/icon_login.png b/export_to_html/image/icon_login.png new file mode 100644 index 0000000..77a6d8c --- /dev/null +++ b/export_to_html/image/icon_login.png @@ -0,0 +1,6 @@ + + + + + + diff --git a/export_to_html/image/icon_login.svg b/export_to_html/image/icon_login.svg new file mode 100644 index 0000000..ce6010b --- /dev/null +++ b/export_to_html/image/icon_login.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/export_to_html/image/icon_map.png b/export_to_html/image/icon_map.png new file mode 100644 index 0000000..015337d --- /dev/null +++ b/export_to_html/image/icon_map.png @@ -0,0 +1,3 @@ + + + diff --git a/export_to_html/image/icon_map.svg b/export_to_html/image/icon_map.svg new file mode 100644 index 0000000..0a37554 --- /dev/null +++ b/export_to_html/image/icon_map.svg @@ -0,0 +1,4 @@ + + + + diff --git a/export_to_html/image/icon_map_fill.svg b/export_to_html/image/icon_map_fill.svg new file mode 100644 index 0000000..cb88225 --- /dev/null +++ b/export_to_html/image/icon_map_fill.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/export_to_html/image/icon_map_pin.svg b/export_to_html/image/icon_map_pin.svg new file mode 100644 index 0000000..7effe98 --- /dev/null +++ b/export_to_html/image/icon_map_pin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/export_to_html/image/icon_menu.png b/export_to_html/image/icon_menu.png new file mode 100644 index 0000000..5d3d770 --- /dev/null +++ b/export_to_html/image/icon_menu.png @@ -0,0 +1,3 @@ + + + diff --git a/export_to_html/image/icon_menu.svg b/export_to_html/image/icon_menu.svg new file mode 100644 index 0000000..6216b1d --- /dev/null +++ b/export_to_html/image/icon_menu.svg @@ -0,0 +1,4 @@ + + + + diff --git a/export_to_html/image/icon_phone.svg b/export_to_html/image/icon_phone.svg new file mode 100644 index 0000000..c5a260f --- /dev/null +++ b/export_to_html/image/icon_phone.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/export_to_html/image/icon_rating_star.png b/export_to_html/image/icon_rating_star.png new file mode 100644 index 0000000..1a91ee6 --- /dev/null +++ b/export_to_html/image/icon_rating_star.png @@ -0,0 +1,5 @@ + + + + + diff --git a/export_to_html/image/icon_rating_star.svg b/export_to_html/image/icon_rating_star.svg new file mode 100644 index 0000000..4155338 --- /dev/null +++ b/export_to_html/image/icon_rating_star.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/export_to_html/image/icon_sale_badge.png b/export_to_html/image/icon_sale_badge.png new file mode 100644 index 0000000..333e6e7 --- /dev/null +++ b/export_to_html/image/icon_sale_badge.png @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/export_to_html/image/icon_search.png b/export_to_html/image/icon_search.png new file mode 100644 index 0000000..592970d --- /dev/null +++ b/export_to_html/image/icon_search.png @@ -0,0 +1,3 @@ + + + diff --git a/export_to_html/image/icon_search.svg b/export_to_html/image/icon_search.svg new file mode 100644 index 0000000..45f7b36 --- /dev/null +++ b/export_to_html/image/icon_search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/export_to_html/image/icon_star_gold.png b/export_to_html/image/icon_star_gold.png new file mode 100644 index 0000000..6df0efc --- /dev/null +++ b/export_to_html/image/icon_star_gold.png @@ -0,0 +1,7 @@ + + + + + + + diff --git a/export_to_html/image/icon_star_gold.svg b/export_to_html/image/icon_star_gold.svg new file mode 100644 index 0000000..4cbfeb0 --- /dev/null +++ b/export_to_html/image/icon_star_gold.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/export_to_html/image/icon_support.svg b/export_to_html/image/icon_support.svg new file mode 100644 index 0000000..2a299de --- /dev/null +++ b/export_to_html/image/icon_support.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/export_to_html/image/icon_youtube.png b/export_to_html/image/icon_youtube.png new file mode 100644 index 0000000..3025bf3 --- /dev/null +++ b/export_to_html/image/icon_youtube.png @@ -0,0 +1,6 @@ + + + + + + diff --git a/export_to_html/image/icon_youtube.svg b/export_to_html/image/icon_youtube.svg new file mode 100644 index 0000000..52e7cf0 --- /dev/null +++ b/export_to_html/image/icon_youtube.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/export_to_html/image/icon_zalo.png b/export_to_html/image/icon_zalo.png new file mode 100644 index 0000000..58ce0df Binary files /dev/null and b/export_to_html/image/icon_zalo.png differ diff --git a/export_to_html/image/logo_bct.png b/export_to_html/image/logo_bct.png new file mode 100644 index 0000000..62fd4f8 Binary files /dev/null and b/export_to_html/image/logo_bct.png differ diff --git a/export_to_html/image/logo_miq.png b/export_to_html/image/logo_miq.png new file mode 100644 index 0000000..44c67c7 Binary files /dev/null and b/export_to_html/image/logo_miq.png differ diff --git a/export_to_html/image/menu_may_giat.png b/export_to_html/image/menu_may_giat.png new file mode 100644 index 0000000..a5c217e Binary files /dev/null and b/export_to_html/image/menu_may_giat.png differ diff --git a/export_to_html/image/menu_may_lanh.png b/export_to_html/image/menu_may_lanh.png new file mode 100644 index 0000000..80618dd Binary files /dev/null and b/export_to_html/image/menu_may_lanh.png differ diff --git a/export_to_html/image/menu_may_nuoc_nong.png b/export_to_html/image/menu_may_nuoc_nong.png new file mode 100644 index 0000000..f89d97b Binary files /dev/null and b/export_to_html/image/menu_may_nuoc_nong.png differ diff --git a/export_to_html/image/menu_may_rua_bat.png b/export_to_html/image/menu_may_rua_bat.png new file mode 100644 index 0000000..f273c62 Binary files /dev/null and b/export_to_html/image/menu_may_rua_bat.png differ diff --git a/export_to_html/image/menu_may_say.png b/export_to_html/image/menu_may_say.png new file mode 100644 index 0000000..6aabe40 Binary files /dev/null and b/export_to_html/image/menu_may_say.png differ diff --git a/export_to_html/image/menu_tivi.png b/export_to_html/image/menu_tivi.png new file mode 100644 index 0000000..e41336a Binary files /dev/null and b/export_to_html/image/menu_tivi.png differ diff --git a/export_to_html/image/menu_tu_dong.png b/export_to_html/image/menu_tu_dong.png new file mode 100644 index 0000000..10880de Binary files /dev/null and b/export_to_html/image/menu_tu_dong.png differ diff --git a/export_to_html/image/menu_tu_lanh.png b/export_to_html/image/menu_tu_lanh.png new file mode 100644 index 0000000..9329018 Binary files /dev/null and b/export_to_html/image/menu_tu_lanh.png differ diff --git a/export_to_html/image/menu_tu_mat.png b/export_to_html/image/menu_tu_mat.png new file mode 100644 index 0000000..c5e0950 Binary files /dev/null and b/export_to_html/image/menu_tu_mat.png differ diff --git a/export_to_html/image/product_badge_1.png b/export_to_html/image/product_badge_1.png new file mode 100644 index 0000000..3acc073 Binary files /dev/null and b/export_to_html/image/product_badge_1.png differ diff --git a/export_to_html/image/product_badge_2.png b/export_to_html/image/product_badge_2.png new file mode 100644 index 0000000..206edfc Binary files /dev/null and b/export_to_html/image/product_badge_2.png differ diff --git a/export_to_html/image/product_card_image.png b/export_to_html/image/product_card_image.png new file mode 100644 index 0000000..4c9bafe Binary files /dev/null and b/export_to_html/image/product_card_image.png differ diff --git a/export_to_html/image/product_history_1.png b/export_to_html/image/product_history_1.png new file mode 100644 index 0000000..55f7872 Binary files /dev/null and b/export_to_html/image/product_history_1.png differ diff --git a/export_to_html/image/product_history_2.png b/export_to_html/image/product_history_2.png new file mode 100644 index 0000000..3b03ce3 Binary files /dev/null and b/export_to_html/image/product_history_2.png differ diff --git a/export_to_html/image/product_history_3.png b/export_to_html/image/product_history_3.png new file mode 100644 index 0000000..ffde216 Binary files /dev/null and b/export_to_html/image/product_history_3.png differ diff --git a/export_to_html/image/product_history_4.png b/export_to_html/image/product_history_4.png new file mode 100644 index 0000000..8a4ebeb Binary files /dev/null and b/export_to_html/image/product_history_4.png differ diff --git a/export_to_html/image/product_sale_image.png b/export_to_html/image/product_sale_image.png new file mode 100644 index 0000000..d4e503a Binary files /dev/null and b/export_to_html/image/product_sale_image.png differ diff --git a/export_to_html/image/sale_price_box_bg.png b/export_to_html/image/sale_price_box_bg.png new file mode 100644 index 0000000..0134915 --- /dev/null +++ b/export_to_html/image/sale_price_box_bg.png @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/export_to_html/image/sale_price_box_bg.svg b/export_to_html/image/sale_price_box_bg.svg new file mode 100644 index 0000000..60c64c9 --- /dev/null +++ b/export_to_html/image/sale_price_box_bg.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/export_to_html/image/search_promo_banner.png b/export_to_html/image/search_promo_banner.png new file mode 100644 index 0000000..0da1e9e Binary files /dev/null and b/export_to_html/image/search_promo_banner.png differ diff --git a/export_to_html/image/trending_gia_dung_1.png b/export_to_html/image/trending_gia_dung_1.png new file mode 100644 index 0000000..a55c433 Binary files /dev/null and b/export_to_html/image/trending_gia_dung_1.png differ diff --git a/export_to_html/image/trending_gia_dung_2.png b/export_to_html/image/trending_gia_dung_2.png new file mode 100644 index 0000000..4704449 Binary files /dev/null and b/export_to_html/image/trending_gia_dung_2.png differ diff --git a/export_to_html/image/trending_gia_dung_3.png b/export_to_html/image/trending_gia_dung_3.png new file mode 100644 index 0000000..0832c18 Binary files /dev/null and b/export_to_html/image/trending_gia_dung_3.png differ diff --git a/export_to_html/image/trending_gia_dung_main.png b/export_to_html/image/trending_gia_dung_main.png new file mode 100644 index 0000000..9ac50b6 Binary files /dev/null and b/export_to_html/image/trending_gia_dung_main.png differ diff --git a/export_to_html/image/trending_mac_mini.png b/export_to_html/image/trending_mac_mini.png new file mode 100644 index 0000000..11d86af Binary files /dev/null and b/export_to_html/image/trending_mac_mini.png differ diff --git a/export_to_html/image/trending_man_hinh.png b/export_to_html/image/trending_man_hinh.png new file mode 100644 index 0000000..d2a3ca9 Binary files /dev/null and b/export_to_html/image/trending_man_hinh.png differ diff --git a/export_to_html/image/trending_may_in_laser.png b/export_to_html/image/trending_may_in_laser.png new file mode 100644 index 0000000..ad343dd Binary files /dev/null and b/export_to_html/image/trending_may_in_laser.png differ diff --git a/export_to_html/image/trending_muc_in.png b/export_to_html/image/trending_muc_in.png new file mode 100644 index 0000000..5de05dd Binary files /dev/null and b/export_to_html/image/trending_muc_in.png differ diff --git a/export_to_html/image/trending_suc_khoe_1.png b/export_to_html/image/trending_suc_khoe_1.png new file mode 100644 index 0000000..2a528c0 Binary files /dev/null and b/export_to_html/image/trending_suc_khoe_1.png differ diff --git a/export_to_html/image/trending_suc_khoe_2.png b/export_to_html/image/trending_suc_khoe_2.png new file mode 100644 index 0000000..99536e3 Binary files /dev/null and b/export_to_html/image/trending_suc_khoe_2.png differ diff --git a/export_to_html/image/trending_suc_khoe_3.png b/export_to_html/image/trending_suc_khoe_3.png new file mode 100644 index 0000000..75bddd4 Binary files /dev/null and b/export_to_html/image/trending_suc_khoe_3.png differ diff --git a/export_to_html/image/trending_suc_khoe_main.png b/export_to_html/image/trending_suc_khoe_main.png new file mode 100644 index 0000000..9711d25 Binary files /dev/null and b/export_to_html/image/trending_suc_khoe_main.png differ diff --git a/export_to_html/image/trending_tivi_2.png b/export_to_html/image/trending_tivi_2.png new file mode 100644 index 0000000..1bde14e Binary files /dev/null and b/export_to_html/image/trending_tivi_2.png differ diff --git a/export_to_html/image/trending_tivi_3.png b/export_to_html/image/trending_tivi_3.png new file mode 100644 index 0000000..5e7fbc6 Binary files /dev/null and b/export_to_html/image/trending_tivi_3.png differ diff --git a/export_to_html/image/trending_tivi_main.png b/export_to_html/image/trending_tivi_main.png new file mode 100644 index 0000000..f2895ef Binary files /dev/null and b/export_to_html/image/trending_tivi_main.png differ diff --git a/export_to_html/image/video_main.png b/export_to_html/image/video_main.png new file mode 100644 index 0000000..004d135 Binary files /dev/null and b/export_to_html/image/video_main.png differ diff --git a/export_to_html/image/video_thumb_1.png b/export_to_html/image/video_thumb_1.png new file mode 100644 index 0000000..13519b0 Binary files /dev/null and b/export_to_html/image/video_thumb_1.png differ diff --git a/export_to_html/image/video_thumb_2.png b/export_to_html/image/video_thumb_2.png new file mode 100644 index 0000000..7c3e68c Binary files /dev/null and b/export_to_html/image/video_thumb_2.png differ diff --git a/export_to_html/index.html b/export_to_html/index.html new file mode 100644 index 0000000..fa27e57 --- /dev/null +++ b/export_to_html/index.html @@ -0,0 +1,1943 @@ + + + + + + + MIQ - Trang Chủ + + + + + + + + + + +
+ + +
+ +
+
+
+ + Sản phẩm Chính hãng - xuất + VAT đầy đủ +
+
+ + Giao nhanh - Miễn phí +
+
+
+ + Hotline: + 0967641641 +
+
+
+ + +
+
+ +
+ MIQ Logo +
+ + + +
+ +
+
+
+ + +
+ + +
+ +
+ + Đăng nhập +
+ +
+ + Giỏ hàng +
+ +
+
+ + Showroom +
+ +
+
+
+
+ + + + + + + + + +
+ + + + + + + +
+
+ + +
+ Banner +
+ + + + + +
+
+ + Sản phẩm đã xem +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+ +
+ +
+
+

Ngập tràn ưu đãi- Đừng bỏ + lỡ!

+
+
+ +
+ TẾT + CHILL - VOUCHER 15% + TIVI + - GIẢM 50% + GIA + DỤNG - 50% + MÁY + GIẶT - 50% + PHỤ + KIỆN - 50% + CÔNG + NGHỆ CAO - 50% + ĐIỀU + HÒA - 50% + TỦ + LẠNH - 50% +
+ +
+ +
+ +
+ +

15.000.000 đ

+

18.000.000 đ (-20%)

+
+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ MUA DEAL HỜI +
+
+
+ +
+ +

15.000.000 đ

+

18.000.000 đ (-20%)

+
+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ MUA DEAL HỜI +
+
+
+ +
+ +

15.000.000 đ

+

18.000.000 đ (-20%)

+
+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ MUA DEAL HỜI +
+
+
+ +
+ +

15.000.000 đ

+

18.000.000 đ (-20%)

+
+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ MUA DEAL HỜI +
+
+
+ +
+ +

15.000.000 đ

+

18.000.000 đ (-20%)

+
+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ MUA DEAL HỜI +
+
+ +
+ +
+ +

15.000.000 đ

+

18.000.000 đ (-20%)

+
+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ MUA DEAL HỜI +
+
+
+ +
+ +

15.000.000 đ

+

18.000.000 đ (-20%)

+
+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ MUA DEAL HỜI +
+
+
+ +
+ +

15.000.000 đ

+

18.000.000 đ (-20%)

+
+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ MUA DEAL HỜI +
+
+
+ +
+ +

15.000.000 đ

+

18.000.000 đ (-20%)

+
+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ MUA DEAL HỜI +
+
+
+ +
+ +

15.000.000 đ

+

18.000.000 đ (-20%)

+
+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ MUA DEAL HỜI +
+
+
+ +
+ Xem thêm deal + + +
+
+
+ + +
+

Top trending

+
+ +
+ +
+
+
+ +
+

Máy in laser trắng...

+
+
+
+ +
+

Màn hình máy tính...

+
+
+
+ +
+

Mac mini ...

+
+
+
+ +
+

Mực in phun...

+
+
+
+ +
+ +
+ +
+ + + +
+
+
+ +
+ +
+ +
+ + + +
+
+
+ +
+ +
+
+ +
+ + + +
+
+
+
+ +
+
+ +
+

Điện Tử - Điện Máy

+

Xem tất cả >>

+
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+
+ +
+
+ +
+

Điện Tử - Điện Máy

+

Xem tất cả >>

+
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+
+ +
+
+ +
+

Điện Tử - Viễn Thông

+

Xem tất cả >>

+
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+
+ +
+
+ +
+

Điện Gia Dụng

+

Xem tất cả >>

+
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+ + Sắm tết vô tư +
+

7.490.000 đ

-20% +
+

7.490.000 đ

+ Máy lạnh Casper + Inverter 1.5 HP GC-12IB36 +
+ + +
+
5 · Đã bán 24,9k
+
+
+ +
+
+
+
+
+ + + + +
+ +
+

TIN KHUYẾN MẠI

+
+ +
+
+ +
+

Ngày hội ĐIỆN MÁY - SIÊU RẺ. Duy nhất 17h + ngày 10/02 Cơ hội trúng Máy nước

+
+ +
+
+ +

Ngày hội ĐIỆN MÁY - SIÊU RẺ. Duy nhất + 17h ngày 10/02 Cơ hội trúng Máy nước

+
+
+ +

Ngày hội ĐIỆN MÁY - SIÊU RẺ. Duy nhất + 17h ngày 10/02 Cơ hội trúng Máy nước

+
+
+ +

Ngày hội ĐIỆN MÁY - SIÊU RẺ. Duy nhất + 17h ngày 10/02 Cơ hội trúng Máy nước

+
+
+
+
+ +
+

VIDEO REVIEW

+
+
+ +
+
+
+
+ +
+
+

Duy nhất 17h ngày 10/02 Cơ + hội trúng Máy nước

+
+
+
+ +
+
+

Duy nhất 17h ngày 10/02 Cơ + hội trúng Máy nước

+
+
+
+
+ + +
+

Mọi người cũng tìm kiếm

+
+ hút mùi hafele + bếp từ cheft + nồi cơm điện Tiger + máy cạo râu Enchen + mũ bảo hiểm + nồi cơm điện Cuckoo + iPhone 15 + chảo chống dính Elmich + máy massage X5 Pro Omni + điều hòa Samsung + máy lọc nước Korihome Kangen K8 + máy massage X5 Pro Omni + máy massage X5 Pro Omni + máy massage X5 Pro Omni + tông đơ cắt tóc + điều hoà Hisense + Máy chạy bộ + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + máy massage X5 Pro Omni + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + điều hòa Nagakawa +
+
+ +
+
+ + +
+
+ +
+

Tổng đài hỗ trợ

+
+

Gọi mua: 0971.49.0000 + (8h00-18h00) +

+

Hỗ trợ kỹ thuật : 0987.762.444 (8h00-18h00)

+

Góp ý - Khiếu nại: 082.418.4444 (8h00-18h00) +

+
+
+ +
+

Về công ty

+
+

Giới thiệu về công ty

+

Tuyển dụng

+

Gửi góp ý, khiếu nại

+

Tìm siêu thị (215 shop)

+
+
+ +
+

Chính sách chung

+
+

Quy định truy cập Website

+

Chính sách bảo mật thông tin

+

Chính sách vận chuyển, giao nhận

+

Chính sách cho doanh nghiệp

+

Chính sách bảo hành, đổi trả

+
+
+ +
+

Kết nối với chúng tôi

+
+
+ +
+
+ Facebook +
+ 12.8k lượt thích +
+ +
+ YouTube + 2.2T Đăng ký +
+ +
+ Zalo + Zalo MIQ +
+
+ Đã thông báo Bộ Công Thương +
+
+
+ +
+

Bản quyền thuộc về MIQ Việt Nam

+
+
+ + + + \ No newline at end of file diff --git a/export_to_html/product-detail.html b/export_to_html/product-detail.html new file mode 100644 index 0000000..f6b9c44 --- /dev/null +++ b/export_to_html/product-detail.html @@ -0,0 +1,495 @@ + + + + + + Chi Tiết Sản Phẩm - MIQ + + + + + + + +
+
+ +
+
+
+ + Sản phẩm Chính hãng - xuất VAT đầy đủ +
+
+ + Giao nhanh - Miễn phí +
+
+
+ + Hotline: 0967641641 +
+
+
+ +
+
+
+ MIQ Logo +
+
+ + Danh mục +
+
+
+
+ + +
+
+ + Đăng nhập +
+
+ + Giỏ hàng +
+
+
+ + Showroom +
+ +
+
+
+
+ + +
+ +
+
+ + +
+
+

Tủ lạnh Toshiba Inverter 711 lít Side By Side GR-RS910WI-PMV(06)-MG

+ Mẫu mới +
+
+ Đã bán 2.3k + + + 4.9 + + + i + Thông số + +
+
+ +
+
+
+
+ Tủ lạnh Toshiba +
+
+ + + + + + + + + + + + + +
+
+ +
+

MIQ cam kết

+
+
+

Hư gì đổi nấy 12 tháng tận nhà (miễn phí tháng đầu) Xem chi tiết

+

Bảo hành chính hãng 2 năm, có người đến tận nhà Xem chi tiết bảo hành

+

Bảo hành máy nén 10 năm (sản phẩm mua từ ngày 01/07/2024)

+

Nếu dùng cho hoạt động kinh doanh (nhà máy, khách sạn, giặt ủi...) thì không được bảo hành.

+

Lắp đặt miễn phí lúc giao hàng

+
+
+ +
+

Tham khảo thêm sản phẩm cũ, trưng bày

+
+ +
+

Tủ lạnh Toshiba Inverter 711 lít Side By Side GR-RS910WI-PMV(06)-MG

+
+

Giá từ:

+

11.590.000₫

+ -50% +
+

Bảo hành Chính hãng đến 15/08/2027

+
+
+
+ +
+

Đặc điểm nổi bật

+
    +
  • Dung tích sử dụng 711 lít phù hợp nhu cầu sử dụng gia đình có 5 thành viên trở lên.
  • +
  • Tiết kiệm điện hiệu quả, tránh lãng phí điện năng nhờ công nghệ Origin Inverter.
  • +
  • Luồng khí lạnh đa chiều Multi Air Flow giúp thực phẩm luôn tươi ngon.
  • +
  • Công nghệ PureAir Turbo giảm thiểu mùi khó chịu trong tủ lạnh.
  • +
  • Kết nối, điều khiển từ xa tiện lợi thông qua ứng dụng TSmartLife.
  • +
+
+ +
+
+ + +
+
+

Tổng quan

+ +
+
+

Kiểu tủ

Tủ lớn - Side by Side - 2 cánh

+

Dung tích tổng:

768 lít

+

Dung tích sử dụng:

711 lít - Trên 5 người

+

Dung tích ngăn đá:

273 lít

+

Dung tích ngăn lạnh:

438 lít

+

Chất liệu khay ngăn lạnh:

Kính chịu lực

+

Chất liệu ống dẫn gas, dàn lạnh:

Ống dẫn gas bằng Sắt và Đồng - Lá tản nhiệt bằng Nhôm

+

Năm ra mắt:

2025

+

Năm ra mắt: 2025 Sản xuất tại:

Trung Quốc

+
+
+ +
+

Đánh giá Tủ lạnh Toshiba Inverter 711 lít Side By Side GR-RS910WI-PMV(06)-MG

+
+
+

4.9/5

+

1,7k khách hài lòng

+

5 đánh giá

+
+
+
5
98.2%
+
4
0
+
3
0
+
2
0
+
1
0
+
+ + + +
+ +
+
+

Nguyễn Lê Tuấn

+ +

Đã mua tại MIQ

+
+

★★★★★ | ❤ Sẽ giới thiệu cho bạn bè, người thân

+

Tủ lạnh có ngăn chứa rất rộng rãi để được nhiều thực phẩm, có app theo dõi rất tiện lợi, mua được ngay dịp sale giá rẻ, có dịp mình sẽ ủng hộ nữa

+
+ +
+ +
+
+

Nguyễn Lê Tuấn

+ +

Đã mua tại MIQ

+
+

★★★★★ | ❤ Sẽ giới thiệu cho bạn bè, người thân

+

Tủ lạnh có ngăn chứa rất rộng rãi để được nhiều thực phẩm, có app theo dõi rất tiện lợi, mua được ngay dịp sale giá rẻ, có dịp mình sẽ ủng hộ nữa

+
+ +
+ + +
+
+
+ + +
+ +
+
+

TIN KHUYẾN MẠI

+
+
+
+ +
+

Ngày hội ĐIỆN MÁY - SIÊU RẺ. Duy nhất 17h ngày 10/02 Cơ hội trúng Máy nước

+
+
+
+ +

Ngày hội ĐIỆN MÁY - SIÊU RẺ. Duy nhất 17h ngày 10/02 Cơ hội trúng Máy nước

+
+
+ +

Ngày hội ĐIỆN MÁY - SIÊU RẺ. Duy nhất 17h ngày 10/02 Cơ hội trúng Máy nước

+
+
+ +

Ngày hội ĐIỆN MÁY - SIÊU RẺ. Duy nhất 17h ngày 10/02 Cơ hội trúng Máy nước

+
+
+
+
+
+

VIDEO REVIEW

+
+
+ +
+
+
+
+ +
+
+

Duy nhất 17h ngày 10/02 Cơ hội trúng Máy nước

+
+
+
+ +
+
+

Duy nhất 17h ngày 10/02 Cơ hội trúng Máy nước

+
+
+
+
+ +
+

Mọi người cũng tìm kiếm

+
+ hút mùi hafele + bếp từ cheft + nồi cơm điện Tiger + máy cạo râu Enchen + mũ bảo hiểm + nồi cơm điện Cuckoo + iPhone 15 + chảo chống dính Elmich + máy massage X5 Pro Omni + điều hòa Samsung + máy lọc nước Korihome Kangen K8 + máy massage X5 Pro Omni + máy massage X5 Pro Omni + máy massage X5 Pro Omni + tông đơ cắt tóc + điều hoà Hisense + Máy chạy bộ + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + máy massage X5 Pro Omni + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + MacBook Air M4 + điều hòa Nagakawa +
+
+
+
+ +
+
+
+

Tổng đài hỗ trợ

+
+

Gọi mua: 0971.49.0000 (8h00-18h00)

+

Hỗ trợ kỹ thuật : 0987.762.444 (8h00-18h00)

+

Góp ý - Khiếu nại: 082.418.4444 (8h00-18h00)

+
+
+
+

Về công ty

+
+

Giới thiệu về công ty

+

Tuyển dụng

+

Gửi góp ý, khiếu nại

+

Tìm siêu thị (215 shop)

+
+
+
+

Chính sách chung

+
+

Quy định truy cập Website

+

Chính sách bảo mật thông tin

+

Chính sách vận chuyển, giao nhận

+

Chính sách cho doanh nghiệp

+

Chính sách bảo hành, đổi trả

+
+
+
+

Kết nối với chúng tôi

+
+
+
+
+ Facebook +
+ 12.8k lượt thích +
+
+ YouTube + 2.2T Đăng ký +
+
+ Zalo + Zalo MIQ +
+
+ Đã thông báo Bộ Công Thương +
+
+
+
+

Bản quyền thuộc về MIQ Việt Nam

+
+
+ + + diff --git a/export_to_html/style.css b/export_to_html/style.css new file mode 100644 index 0000000..aef367a --- /dev/null +++ b/export_to_html/style.css @@ -0,0 +1,88 @@ +/* ===== Base ===== */ +body { font-family: 'Roboto', sans-serif; margin: 0; } +.font-inter { font-family: 'Inter', sans-serif; } + +/* ===== Mega menu — left panel items ===== */ +.cat-item { transition: background 0.1s; } +.cat-item:hover { background: white; } +.cat-item.cat-active { background: white; } +.cat-item .cat-label { color: #1a1a1a; font-weight: 400; } +.cat-item.cat-active .cat-label { color: #90004b; font-weight: 600; } +.cat-item .cat-chevron { display: none; } +.cat-item:hover .cat-chevron, +.cat-item.cat-active .cat-chevron { display: block; } + +/* ===== Mega menu — right panel subcategory grid ===== */ +.menu-subcat-grid { + display: grid; + grid-template-columns: repeat(6, 1fr); + row-gap: 20px; + padding: 18px 24px 0; + flex: 1; + overflow-y: auto; + align-content: start; +} + +.menu-subcat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + transition: opacity 0.15s; + text-decoration: none; +} +.menu-subcat-item:hover { opacity: 0.75; } +.menu-subcat-item span { + font-size: 12px; + color: #000; + text-align: center; + line-height: 1.3; +} + +/* ===== Mega menu — right panel brand row ===== */ +.menu-brand-row { + display: flex; + align-items: center; + gap: 10px; + padding: 16px 24px 0; + border-top: 1px solid #d3d7df; + flex-shrink: 0; + margin-top: auto; +} + +.menu-brand-item { + display: flex; + align-items: center; + gap: 4px; + background: white; + border-radius: 6px; + height: 36px; + padding: 0 8px; +} + +.menu-brand-num { + font-weight: 700; + font-size: 20px; + color: white; + line-height: 1; + text-shadow: 1px 1px 2.1px #0067c7; +} + +/* ===== Mega menu — right panel container ===== */ +.menu-rp-panel { + position: absolute; + inset: 0; + flex-direction: column; + background: white; +} + +/* ===== Mega menu — right panel title bar ===== */ +.menu-rp-title { + border-bottom: 1px solid #d3d7df; + padding: 12px 24px 10px; + flex-shrink: 0; + font-size: 13px; + font-weight: 600; + color: #000; + text-transform: uppercase; +} diff --git a/inc/Hura8/AdminTemplateFilter.php b/inc/Hura8/AdminTemplateFilter.php new file mode 100644 index 0000000..8c9ee45 --- /dev/null +++ b/inc/Hura8/AdminTemplateFilter.php @@ -0,0 +1,261 @@ + '', 'value' => ] from [key1 => value1, key2=>value2, ...] + * + * @param array $key_values [key1 => value1, key2=>value2] + * + * @return array [['key' => 'key1', 'value' => value1], ['key' => 'key2', 'value' => value2]] + */ + public static function to_array(array $key_values) { + $result = []; + foreach ($key_values as $key => $value) { + $result[] = [ + 'key' => $key, + 'value' => $value, + ]; + } + + return $result; + } + + + /** + * split a s by line to create array + * + * @param string $txt + * + * @return array + */ + public static function get_line($txt) { + + if(is_array($txt)) { + return $txt; + } + + $txt = trim($txt); + if( ! $txt ) return []; + + return preg_split("/\n/", $txt); + } + + /** + * Implement strlen + * + * @param string $str + * + * @return int + */ + public static function length($str) { + return strlen(trim($str)); + } + + + /** + * Make number easier to read: 1000 -> 1.000 + * + * @param string $number + * + * @return string + */ + public static function format_number($number) { + if(!$number) return ''; + $number = floatval($number); + + $number = number_format($number, 0, ",", "."); //Vietnamese format with decimals by a coma + + return $number; + } + + public static function format_price($p_price, $currency = ''){ + if(!$p_price) return ''; + if(!$currency) $currency = (defined("DEFAULT_CURRENCY")) ? DEFAULT_CURRENCY : "vnd"; + //if(is_string($p_price)) return 0; + if($currency == 'usd') { + return number_format($p_price,2,".",","); + }else { + return number_format($p_price,0,",","."); + } + } + + public static function global_asset_url($file_name = '') + { + return GLOBAL_ASSETS_PATH . $file_name; + } + + /** + * + * Description: get the shop's full asset url for template's images/js/css + * + * //Returns the URL of a file in the "assets" folder of a theme. + // {{ 'shop.css' | asset_url : 'arg1', 'arg2' ...}} -> //cdn.shopify.com/s/files/1/0087/0462/t/394/assets/shop.css?28253 + * + * @param string $file_name + * + * @return string + */ + public static function asset_url($file_name = '') + { + if( !$file_name ) return ''; + + $file_ext = strtolower(strrchr($file_name, ".")); + + // script tags + if(in_array($file_ext, ['.js', '.css'])) return TEMPLATE_ASSET . "/script/" . $file_name; + + // default image + return TEMPLATE_ASSET . "/images/" . $file_name; + } + + + /** + * + * Description: construct a full html tag for images/js/css file + * + * @param string $file_path domain.com/static/style.css?v=3.1.1 + * + * @return string + */ + public static function script_tag($file_path) { + if( ! $file_path ) return ''; + + //check for ? + if(strpos($file_path, "?") !== false) { + $file_ext = str_replace(strrchr($file_path, "?"), "", $file_path); + $file_ext = strtolower(strrchr($file_ext, ".")); + } else { + $file_ext = strtolower(strrchr($file_path, ".")); + } + + $tag_config = [ + ".css" => "", + ".js" => "", + ".jpg" => "\"n\"/", + ".jpeg" => "\"\"/", + ".gif" => "\"\"/", + ".png" => "\"\"/", + ]; + + return (isset($tag_config[$file_ext])) ? $tag_config[$file_ext] : ''; + } + + /** + * {{ product_info.main_image | img_url: '300x300' }} => https://cdn.shopify.com/s/files/1/1183/1048/products/boat-shoes_300x300.jpeg?1459175177 + * @param string $full_path + * @param string $modifier + * $modifier: + * - must be in format: NumberxNumber or Numberx where Number must within 10 -> 9999 + * - or be one of these: small | medium | large + * @return string + */ + public static function img_url($full_path, $modifier) + { + $clean_modifier = ($modifier) ? trim($modifier) : ""; + + // verify $modifier + // must be in format: NumberxNumber or Numberx where Number must within 10 -> 9999 + if($clean_modifier + && !preg_match("/^[0-9]{2,4}x([0-9]{2,4})?$/i", $clean_modifier) + && !in_array($clean_modifier, ["small", "medium", "large"]) + ) { + $clean_modifier = ""; + } + + // return if no valid modifier + if( ! $clean_modifier ) { + return $full_path; + } + + $last_dot_position = strrpos($full_path, "."); + if( ! $last_dot_position ) return $full_path . $clean_modifier; + + return join("", [ + substr($full_path, 0, $last_dot_position), + "_", + $clean_modifier, + substr($full_path, $last_dot_position) + ]); + } + + /** + * //Returns the URL of a file in the Files page of the admin. + //{{ 'size-chart.pdf' | file_url }} -> //cdn.shopify.com/s/files/1/0087/0462/files/size-chart.pdf?28261 + * + * @param string $input + * @param string $string + * + * @return string + */ + public static function file_url($input, $string) + { + return strtoupper($input) . " = " . $string; + } + + /** + * //Returns the asset URL of an image in the Files page of the admin. file_img_url accepts an image size parameter. + //{{ 'logo.png' | file_img_url: '1024x768' }} -> //cdn.shopify.com/s/files/1/0246/0527/files/logo_1024x768.png?42 + * + * @param string $input + * @param string $string + * + * @return string + */ + public static function file_img_url($input, $string) + { + return ''; + } + + + /** + * Show all content of a variable, useful for template development + * + * @param string + * + * @return string + */ + public static function print_r($input) + { + @ob_start(); + print_r($input); + $content = ob_get_contents(); + @ob_end_clean(); + + return join("\r", ['']) ; + } + + /** + * Show all content of a variable, useful for template development + * + * @param string + * + * @return string + */ + public static function show_var($input) + { + @ob_start(); + print_r($input); + $content = ob_get_contents(); + @ob_end_clean(); + + return join("\r", ['']) ; + } +} diff --git a/inc/Hura8/AppAdmin.php b/inc/Hura8/AppAdmin.php new file mode 100644 index 0000000..79c944d --- /dev/null +++ b/inc/Hura8/AppAdmin.php @@ -0,0 +1,223 @@ + 'home', + "view" => 'home', + "url" => '/product' + ]; + + protected $data = []; + + + public function __construct() + { + + } + + // start the app + public function start() { + $this->getRouter(); + $this->getData(); + echo $this->renderModule(); + } + + + protected function getRouter() { + /*$route = [ + "module" => (isset($_REQUEST['module'])) ? $_REQUEST['module'] : 'home', + "view" => (isset($_REQUEST['view'])) ? $_REQUEST['view'] : 'home', + ];*/ + + $objRouter = new Router(); + $this->current_route_info = $objRouter->getRouting(); + } + + + protected function getData() { + $module_file = $this->getModuleFile(); + + if(file_exists($module_file)) { + // print_r($this->current_route_info); + // die('Page '. $module_file .' not found!'); + $data = include_once $module_file; + }else{ + $data = ['file data '. $module_file .' not found!']; + } + + $global_data = [ + "module" => $this->current_route_info['module'], + "view" => $this->current_route_info['view'], + "url" => $this->current_route_info['url'], + ]; + + $component_data = [ + "module" => $this->current_route_info['module'], + "view" => $this->current_route_info['view'], + "url" => $this->current_route_info['url'], + "main_menu" => include_once ROOT_DIR."/data/menu.php", + ]; + + $banner_data = [ + "banner" => include_once ROOT_DIR."/data/banner/banner.php", + ]; + + $this->data = array( + 'global' => $global_data, + 'banner' => $banner_data, + 'component' => $component_data, + 'page' => (is_array($data)) ? $data : [], + ); + } + + + protected function getModuleFile() { + return join(DIRECTORY_SEPARATOR, [ + "data", + $this->current_route_info["module"], + str_replace("-", "_", $this->current_route_info["view"]).".php" + ]) ; + } + + + protected function renderModule() { + + if(!$this->current_route_info['module'] || !$this->current_route_info['view']) { + die("Module not exist"); + } + + $template_file_path = $this->tpl_path ."/". $this->current_route_info['module']; + $template_file_name = str_replace("-", '_', $this->current_route_info['view']).".html"; + $template_file_full_path = $template_file_path."/".$template_file_name; + + //check exist + if(!@file_exists( $template_file_full_path)) { + // attempt to auto create first + // todo: this MUST BE TURNED OFF IN PRODUCTION, else many files will be created unintentionally + $module_file = $this->getModuleFile(); + // only create if module file exist + if(file_exists($module_file) && !$this->autoCreateTplFile( $template_file_path, $template_file_name )) { + die("Please manually create template file at: ". $template_file_full_path); + } + } + + $theme_file_path = $this->tpl_path ."/theme.html"; + if( ! @file_exists( $theme_file_path)) { + die("Theme not exist (please create): " . $theme_file_path); + } + + $theme_content = @file_get_contents( $theme_file_path ); + $module_content = @file_get_contents( $template_file_full_path ); + + $page_content_to_parse = preg_replace([ + "/{{(\s+)?page_content(\s+)?}}/" + ], [ + $module_content, + ] , $theme_content ); + + + return $this->parse( + $page_content_to_parse, + $template_file_path + ); + } + + + protected function autoCreateTplFile($file_path, $file_name) : bool { + // create dir if not exist + if(!file_exists($file_path)) { + if(!mkdir($file_path, 0755, true)) { + return false; + } + + if(!file_exists($file_path)) { + return false; + } + } + + //create file + $file_full_path = $file_path . "/". $file_name; + @file_put_contents($file_full_path, $file_full_path); + + return file_exists($file_full_path); + } + + + /* + * 2 ways to render a html template + * 1. Use $html_to_parse, which requires no dependencies + * Example: + * Template::parse(null, 'Age = {{age}}', ['age' => 21], ''); + * + * 2. Use $template_file_path, which requires dependency $path + * Template::parse(Template::$setting_template_path, null, ['age' => 21], 'email/test'); + * */ + protected function parse($html_to_parse = null, $template_file_path = '') { + + if(!$html_to_parse && !$template_file_path) { + return 'Nothing to parse'; + } + + //output to html + Liquid::set('INCLUDE_SUFFIX', 'html'); + Liquid::set('INCLUDE_PREFIX', ''); + //Liquid::set('INCLUDE_ALLOW_EXT', true); + Liquid::set('ESCAPE_BY_DEFAULT', false); + + $enable_cache = false; // default = true, turn this on-off to disable cache while working on local mode + //$enable_cache = true; + + //catch exception and print friendly notice + try { + + $objLiquidTemplate = new LiquidTemplate( $this->tpl_path ); + $objLiquidTemplate->registerFilter( AdminTemplateFilter::class ); + if($enable_cache) { + /*$objLiquidTemplate->setCache(new File([ + 'cache_dir' => self::$cache_dir + ]));*/ + } + + if($html_to_parse) { + $objLiquidTemplate->parse($html_to_parse); + }elseif ($template_file_path) { + $objLiquidTemplate->parseFile($template_file_path); + } + + return $objLiquidTemplate->render($this->data); + + } catch (\Exception $e) { + $result = []; + do { + //printf("%s:%d %s (%d) [%s]\n", $e->getFile(), $e->getLine(), $e->getMessage(), $e->getCode(), get_class($e)); + //echo $e->getTraceAsString(); + //$code = $e->getTrace()[0]['args'][0]; + //if(is_array($code)) $code = serialize($code); + $result[] = sprintf( + " +Lỗi code trong file template html:
+- Chi tiết lỗi: %s
+- File template: %s
+- Hướng dẫn xử lý: Tách từng phần html để kiểm tra và nhấn F5 mỗi lần. Nếu không xuất hiện thông báo này nghĩa là phần đó không tạo lỗi +", + $e->getMessage(), + substr($template_file_path, strrpos($template_file_path, DIRECTORY_SEPARATOR) + 1 ), + //static::$cache_dir + ); + + } while($e = $e->getPrevious()); + + return join(" - ", $result); + } + } + +} diff --git a/inc/Hura8/Components/Analytics/Controller/bTrackingController.php b/inc/Hura8/Components/Analytics/Controller/bTrackingController.php new file mode 100644 index 0000000..b52f119 --- /dev/null +++ b/inc/Hura8/Components/Analytics/Controller/bTrackingController.php @@ -0,0 +1,17 @@ +objTrackingModel = new TrackingModel(); + } + +} diff --git a/inc/Hura8/Components/Analytics/Model/TrackDeviceInfo.php b/inc/Hura8/Components/Analytics/Model/TrackDeviceInfo.php new file mode 100644 index 0000000..a0df313 --- /dev/null +++ b/inc/Hura8/Components/Analytics/Model/TrackDeviceInfo.php @@ -0,0 +1,19 @@ +ip_address = $ip_address; + $this->user_agent = $user_agent; + $this->referrer = $referrer; + $this->is_mobile = $is_mobile; + } +} diff --git a/inc/Hura8/Components/Analytics/Model/TrackRouteInfo.php b/inc/Hura8/Components/Analytics/Model/TrackRouteInfo.php new file mode 100644 index 0000000..e68b8bc --- /dev/null +++ b/inc/Hura8/Components/Analytics/Model/TrackRouteInfo.php @@ -0,0 +1,28 @@ +url = $url; + $this->module = $module; + $this->view = $view; + $this->view_id = $view_id; + $this->query = $query; + } + +} diff --git a/inc/Hura8/Components/Analytics/Model/TrackUserInfo.php b/inc/Hura8/Components/Analytics/Model/TrackUserInfo.php new file mode 100644 index 0000000..21243a1 --- /dev/null +++ b/inc/Hura8/Components/Analytics/Model/TrackUserInfo.php @@ -0,0 +1,19 @@ +web_user_id = $web_user_id; + $this->customer_id = $customer_id; + $this->is_crawler = $is_crawler ? 1 : 0; + } + +} diff --git a/inc/Hura8/Components/Analytics/Model/TrackingModel.php b/inc/Hura8/Components/Analytics/Model/TrackingModel.php new file mode 100644 index 0000000..5ec20d0 --- /dev/null +++ b/inc/Hura8/Components/Analytics/Model/TrackingModel.php @@ -0,0 +1,32 @@ +isDefaultLanguage()) { + return $this->iEntityLanguageModel->update($item_id, $new_item_info); + } + + return $this->objArticleModel->updateTableInfo($item_id, $new_item_info); + } + +} diff --git a/inc/Hura8/Components/Article/Controller/bArticleCategoryController.php b/inc/Hura8/Components/Article/Controller/bArticleCategoryController.php new file mode 100644 index 0000000..dbd2202 --- /dev/null +++ b/inc/Hura8/Components/Article/Controller/bArticleCategoryController.php @@ -0,0 +1,35 @@ +objArticleCategoryModel = new ArticleCategoryModel(); + + if(!$this->isDefaultLanguage()) { + + parent::__construct( + $this->objArticleCategoryModel, + new ArticleCategoryLanguageModel() + ); + + } else { + + parent::__construct($this->objArticleCategoryModel); + + } + } + + +} diff --git a/inc/Hura8/Components/Article/Controller/bArticleController.php b/inc/Hura8/Components/Article/Controller/bArticleController.php new file mode 100644 index 0000000..b3ed6be --- /dev/null +++ b/inc/Hura8/Components/Article/Controller/bArticleController.php @@ -0,0 +1,91 @@ + ['width' => 200,] , + 'l' => ['width' => 600,] , + ); + + /* @var ArticleModel $objArticleModel */ + protected $objArticleModel; + + public function __construct() + { + $this->objArticleModel = new ArticleModel(); + + if(!$this->isDefaultLanguage()) { + parent::__construct( + $this->objArticleModel, + new ArticleLanguageModel() + ); + + } else { + parent::__construct($this->objArticleModel); + } + } + + + public function getFullInfo($id) + { + + if(!$id) return null; + + return self::getCache("getFullInfo-".$id."-".$this->view_language, function () use ($id){ + + $info = $this->objArticleModel->getFullInfo($id); + + if($this->iEntityLanguageModel && $info ) { + $item_language_info = $this->iEntityLanguageModel->getInfo($id) ?? ["not_translated" => true]; + return $this->formatItemInfo(array_merge($info, $item_language_info)); + } + + return ($info) ? $this->formatItemInfo($info) : null; + + }); + } + + + protected function formatItemInList(array $item_info) + { + return $this->formatItemInfo($item_info); + } + + + protected function formatItemInfo(array $item_info) + { + if(!$item_info) return null; + + $info = $item_info; + $info['image'] = self::getResizedImageCollection($info['thumbnail']); + + return $info; + } + + + public static function getResizedImageCollection($image_name) { + $image = []; + + $size_in_full = [ + 't' => 'thumb' , + 's' => 'small' , + 'l' => 'large' , + ]; + + foreach (static::$resized_sizes as $size => $value) { + $image[$size_in_full[$size]] = ($image_name) ? STATIC_DOMAIN . "/". static::$image_folder . "/". $size. IMAGE_FILE_SEPARATOR . $image_name : ''; + } + + return $image; + } + +} diff --git a/inc/Hura8/Components/Article/Model/ArticleCategoryLanguageModel.php b/inc/Hura8/Components/Article/Model/ArticleCategoryLanguageModel.php new file mode 100644 index 0000000..8b37216 --- /dev/null +++ b/inc/Hura8/Components/Article/Model/ArticleCategoryLanguageModel.php @@ -0,0 +1,21 @@ +richtext_fields); + } + +} diff --git a/inc/Hura8/Components/Article/Model/ArticleCategoryModel.php b/inc/Hura8/Components/Article/Model/ArticleCategoryModel.php new file mode 100644 index 0000000..c367bed --- /dev/null +++ b/inc/Hura8/Components/Article/Model/ArticleCategoryModel.php @@ -0,0 +1,33 @@ +richtext_fields); + } + +} diff --git a/inc/Hura8/Components/Article/Model/ArticleModel.php b/inc/Hura8/Components/Article/Model/ArticleModel.php new file mode 100644 index 0000000..1c5771f --- /dev/null +++ b/inc/Hura8/Components/Article/Model/ArticleModel.php @@ -0,0 +1,150 @@ +db->runQuery( + "SELECT * FROM `".$this->tb_entity."` basic, `".$this->tb_article_info."` info + WHERE basic.`id` = info.`article_id` AND basic.id = ? + LIMIT 1 ", + ['d'], [$id] + ); + + if( $item_info = $this->db->fetchAssoc($query)){ + return $item_info; + } + + return null; + } + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + /*$condition = array( + "category" => getRequestInt("category"), + "no_image" => 0,//1 + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + //Tim danh muc + if(isset($filter_condition["category"]) && $filter_condition["category"]) { + + $objArticleCategoryModel = new ArticleCategoryModel(); + $category_info = $objArticleCategoryModel->getInfo($filter_condition["category"]); + + if($category_info) { + if($category_info['is_parent']) { + $catCondition[] = " AND `id` IN (SELECT `item_id` FROM `".$this->tb_article_per_category."` WHERE `category_id` IN (".$category_info['child_ids'].") ) "; + //$bind_types[] = 'd'; + //$bind_values[] = $filter_condition["category"]; + }else{ + $catCondition[] = " AND `id` IN (SELECT `item_id` FROM `".$this->tb_article_per_category."` WHERE `category_id` = ? ) "; + $bind_types[] = 'd'; + $bind_values[] = $filter_condition["category"]; + } + } + + } + + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function addArticleToCategory($item_id, array $category_list_id) { + $this->db->runQuery("DELETE FROM `".$this->tb_article_per_category."` WHERE `item_id` = ? ", ['d'], [$item_id]); + + $bulk_inserts = []; + foreach($category_list_id as $cat_id) { + if (! $cat_id) continue; + + $bulk_inserts[] = [ + 'category_id' => $cat_id, + 'item_id' => $item_id, + 'status' => 1, + 'create_time' => CURRENT_TIME, + ]; + } + + if(sizeof($bulk_inserts)) { + $this->db->bulk_insert($this->tb_article_per_category, $bulk_inserts); + } + + // update counter + $objArticleCategoryModel = new ArticleCategoryModel(); + foreach($category_list_id as $cat_id) { + $objArticleCategoryModel->updateItemCount($cat_id); + } + + } + + + public function updateUrl($id, $url_index): bool + { + + $module_routing = ModuleManager::getModuleRouting("article"); + $request_path_config = isset($module_routing["detail"]) ? $module_routing["detail"]['url_manager']['request_path'] : ''; + + if(!$request_path_config) { + return false; + } + + $request_path = UrlManagerController::translateRequestPathConfig($request_path_config, $id, $url_index); + $id_path = UrlManagerController::createIdPath("article", "detail", $id); + + $objUrlManager = new UrlManagerController(); + $new_request_path = $objUrlManager->createUrl("article:detail", $request_path, $id_path, 0); + + if($new_request_path) { + $this->db->update( + $this->tb_entity, + [ + 'request_path' => $new_request_path, + ], + [ + 'id' => $id, + ] + ); + } + + return true; + } + + + +} diff --git a/inc/Hura8/Components/Article/Model/ArticleSearchModel.php b/inc/Hura8/Components/Article/Model/ArticleSearchModel.php new file mode 100644 index 0000000..0daa06c --- /dev/null +++ b/inc/Hura8/Components/Article/Model/ArticleSearchModel.php @@ -0,0 +1,29 @@ + "tb_article.status", + ]; + + private $fulltext_fields = [ + "keywords" => ["tb_article.title", ], + ]; + + + public function __construct() + { + parent::__construct( + "tb_article", + $this->fulltext_fields, + $this->filter_fields + ); + } + +} diff --git a/inc/Hura8/Components/Article/Model/UArticleModel.php b/inc/Hura8/Components/Article/Model/UArticleModel.php new file mode 100644 index 0000000..52d849b --- /dev/null +++ b/inc/Hura8/Components/Article/Model/UArticleModel.php @@ -0,0 +1,55 @@ +db->runQuery(" + ( + SELECT `item_id` + FROM ".$this->tb_article_per_category." + WHERE `category_id` = ? AND `status`=1 AND `item_id` > ? + ORDER BY `item_id` DESC + LIMIT 10 + + ) UNION ALL ( + SELECT `item_id` + FROM ".$this->tb_article_per_category." + WHERE `category_id` = ? AND `status`=1 AND `item_id` < ? + ORDER BY `item_id` DESC + LIMIT 10 + ) + ", + ['d', 'd', 'd', 'd'], + [$category_id, $main_id, $category_id, $main_id] + ); + + $article_list_id = []; + $article_item_info = array(); + $article_item = []; + + foreach ( $this->db->fetchAll($query) as $rs ) { + if(!isset($article_item_info[$rs["item_id"]])) $article_item_info[$rs["item_id"]] = array(); + if(!in_array($rs["item_id"], $article_list_id)) $article_list_id[] = $rs["item_id"]; + + if($rs["item_id"] > $main_id) { + $article_item['new'][$rs["item_id"]] = &$article_item_info[$rs["item_id"]]; + } + else { + $article_item['old'][$rs["item_id"]] = &$article_item_info[$rs["item_id"]]; + } + } + + $list_article_info = $this->getListByIds($article_list_id); + foreach ($article_list_id as $_id) { + if(isset($list_article_info[$_id])) $article_item_info[$_id] = $list_article_info[$_id]; + } + + + return $article_item; + } + +} diff --git a/inc/Hura8/Components/Banner/AdminController/ABannerController.php b/inc/Hura8/Components/Banner/AdminController/ABannerController.php new file mode 100644 index 0000000..ee1196a --- /dev/null +++ b/inc/Hura8/Components/Banner/AdminController/ABannerController.php @@ -0,0 +1,16 @@ + "Toàn bộ website" , + "header" => "Đầu trang" , + "homepage" => "Trang chủ" , + "column_left" => "Cột trái" , + "column_right" => "Cột phải" , + "footer" => "Chân trang" , + "product_detail"=> "Chi tiết sản phẩm" , + "product_list" => "Danh sách & Danh mục sản phẩm" , + "collection_list" => "Bộ sưu tập" , + "article_home" => "Trang chủ tin tức" , + "brand_detail" => "Chi tiết thương hiệu", + ); + + + protected $objBannerModel; + protected $objBannerLocationModel; + + public function __construct() + { + $this->objBannerModel = new BannerModel(); + $this->objBannerLocationModel = new BannerLocationModel(); + + parent::__construct($this->objBannerModel); + } + + + protected function formatItemInList(array $item_info) + { + return self::formatFile($item_info); + } + + protected function formatItemInfo(array $item_info) + { + return self::formatFile($item_info); + } + + + public static function formatFile(array $item_info) + { + + if($item_info['file_url']) { + $item_info['display_file'] = STATIC_DOMAIN ."/". static::$image_folder ."/". $item_info['file_url']; + }else if($item_info['file_external_url']) { + $item_info['display_file'] = $item_info['file_external_url']; + } + + $item_info['html_code'] = "\"".htmlspecialchars($item_info['title'])."\""; + + return $item_info; + } + + +} diff --git a/inc/Hura8/Components/Banner/Model/BannerLocationModel.php b/inc/Hura8/Components/Banner/Model/BannerLocationModel.php new file mode 100644 index 0000000..d3208cb --- /dev/null +++ b/inc/Hura8/Components/Banner/Model/BannerLocationModel.php @@ -0,0 +1,30 @@ +db->runQuery("SELECT * FROM `".$this->tb_entity."` WHERE `tracking_id` = ? LIMIT 1 ", ['s'], [$tracking_id]) ; + if( $item_info = $this->db->fetchAssoc($query)){ + return $this->formatItemInfo($item_info); + } + + return false; + } + + public function getBannerPerTemplate(array $template_list, $numberOfBannerPerTpl=100){ + + $all_bind_types = []; + $all_bind_values = []; + + $view_id = 0; + + $build_query = []; + foreach($template_list as $tpl) { + + list($where_condition, $bind_types, $bind_values) = $this->buildQueryPerTpl($tpl, $view_id, $numberOfBannerPerTpl); + + $build_query[] = " (".$where_condition.") "; + $all_bind_types = array_merge($all_bind_types, $bind_types); + $all_bind_values = array_merge($all_bind_values, $bind_values); + } + + if(!sizeof($build_query)) return []; + + $query = $this->db->runQuery(join(" UNION ALL ", $build_query), $all_bind_types, $all_bind_values); + + return $this->db->fetchAll($query); + } + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + /*$condition = array( + [location] => 2 + [category] => 0 + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + if(isset($filter_condition['location']) && $filter_condition['location']) { + $catCondition[] = " AND `location` = ? "; + $bind_types[] = 'd'; + $bind_values[] = $filter_condition['location']; + } + + if(isset($filter_condition['category']) && $filter_condition['category']) { + $catCondition[] = " AND `id` IN ( SELECT `banner_id` FROM `".$this->tb_banner_per_category."` WHERE `category_id` = ? ) "; + $bind_types[] = 'd'; + $bind_values[] = $filter_condition['category']; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + +} diff --git a/inc/Hura8/Components/Banner/Model/BannerSearchModel.php b/inc/Hura8/Components/Banner/Model/BannerSearchModel.php new file mode 100644 index 0000000..6fd6b38 --- /dev/null +++ b/inc/Hura8/Components/Banner/Model/BannerSearchModel.php @@ -0,0 +1,33 @@ + "tb_banner.location", + "status" => "tb_banner.status", + ]; + + private $fulltext_fields = [ + "keywords" => ["tb_banner.title",], + ]; + + + public function __construct() + { + parent::__construct( + "tb_banner", + $this->fulltext_fields, + $this->filter_fields + ); + + //$this->createTableSearch(); + } + +} diff --git a/inc/Hura8/Components/Brand/AdminController/ABrandController.php b/inc/Hura8/Components/Brand/AdminController/ABrandController.php new file mode 100644 index 0000000..c56efb1 --- /dev/null +++ b/inc/Hura8/Components/Brand/AdminController/ABrandController.php @@ -0,0 +1,44 @@ +objBrandModel->getGroupByFirstLetter(); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // delete thumb files + $item_info = $this->getInfo($item_id); + if($item_info['thumbnail']) { + foreach (static::$resized_sizes as $size => $value) { + $file_local_path = PUBLIC_DIR . "/". static::$image_folder . "/". $size. IMAGE_FILE_SEPARATOR . $item_info['thumbnail']; + unlink($file_local_path); + } + + // remove original file + $file_local_path = PUBLIC_DIR . "/". static::$image_folder . "/". $item_info['thumbnail']; + unlink($file_local_path); + } + + //delete media files? + // todo: + + // ok + return true; + } + + +} diff --git a/inc/Hura8/Components/Brand/Controller/bBrandController.php b/inc/Hura8/Components/Brand/Controller/bBrandController.php new file mode 100644 index 0000000..8376b0e --- /dev/null +++ b/inc/Hura8/Components/Brand/Controller/bBrandController.php @@ -0,0 +1,73 @@ + ['width' => 200,] , + ); + + /* @var BrandModel $objBrandModel */ + protected $objBrandModel; + /* @var BrandLanguageModel $objBrandLanguageModel */ + protected $objBrandLanguageModel; + protected $view_language = ''; + + public function __construct() + { + $this->objBrandModel = new BrandModel(); + + if(!$this->isDefaultLanguage()) { + $this->objBrandLanguageModel = new BrandLanguageModel(); + //$this->objVideoLanguageModel->createTableLang(); + parent::__construct($this->objBrandModel, $this->objBrandLanguageModel); + + }else{ + parent::__construct($this->objBrandModel); + } + } + + public function getInfoByUrl(string $band_index) : ?array + { + return $this->objBrandModel->getInfoByUrl($band_index); + } + + protected function formatItemInList(array $item_info) : array + { + return $this->formatItemInfo($item_info); + } + + + protected function formatItemInfo(array $item_info) : ?array + { + if(!$item_info) return null; + + $info = static::formatItemImage($item_info); + + $info['url'] = "/brand/".$info['brand_index']; + + return $info; + } + + + public static function formatItemImage(array $item_info) { + $info = $item_info; + + foreach (static::$resized_sizes as $size => $value) { + $info['image'][$size] = ($info['thumbnail']) ? STATIC_DOMAIN . "/". static::$image_folder . "/". $size. IMAGE_FILE_SEPARATOR . $info['thumbnail'] : ''; + } + + return $info; + } + + +} diff --git a/inc/Hura8/Components/Brand/Model/BrandLanguageModel.php b/inc/Hura8/Components/Brand/Model/BrandLanguageModel.php new file mode 100644 index 0000000..543df94 --- /dev/null +++ b/inc/Hura8/Components/Brand/Model/BrandLanguageModel.php @@ -0,0 +1,19 @@ +richtext_fields); + } + +} diff --git a/inc/Hura8/Components/Brand/Model/BrandModel.php b/inc/Hura8/Components/Brand/Model/BrandModel.php new file mode 100644 index 0000000..344e4a2 --- /dev/null +++ b/inc/Hura8/Components/Brand/Model/BrandModel.php @@ -0,0 +1,72 @@ +db->runQuery( + "SELECT `letter`, COUNT(*) AS item_count FROM `".$this->tb_entity."` GROUP BY `letter` ORDER BY `letter` ASC " + ); + return $this->db->fetchAll($query); + } + + + protected function _buildQueryConditionExtend(array $filter_condition): ?array + { + /*$condition = array( + "letter" => "", + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + if(isset($filter_condition["letter"]) && strlen($filter_condition["letter"]) == 1){ + $catCondition[] = " AND `letter` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["letter"]; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + public function getInfoByUrl($brand_index) : ?array + { + $brand_index = preg_replace("/[^a-z0-9\.\-\_]/i", '', $brand_index); + + $query = $this->db->runQuery("SELECT * FROM `".$this->tb_entity."` WHERE `brand_index` = ? LIMIT 1 ", ['s'], [$brand_index]); + if($item_info = $this->db->fetchAssoc($query)){ + return $this->formatItemInfo($item_info); + } + + return null; + } + +} diff --git a/inc/Hura8/Components/ComboSet/AdminController/AComboSetController.php b/inc/Hura8/Components/ComboSet/AdminController/AComboSetController.php new file mode 100644 index 0000000..f64db01 --- /dev/null +++ b/inc/Hura8/Components/ComboSet/AdminController/AComboSetController.php @@ -0,0 +1,15 @@ +objComboSetModel = new ComboSetModel(); + + if(!$this->isDefaultLanguage()) { + $this->objComboSetLanguageModel = new ComboSetLanguageModel(); + //$this->objVideoLanguageModel->createTableLang(); + parent::__construct($this->objComboSetModel, $this->objComboSetLanguageModel); + + }else{ + parent::__construct($this->objComboSetModel); + } + } + + + public function getAllSetIdsForAProduct($product_id) + { + return $this->objComboSetModel->getAllSetIdsForAProduct($product_id); + } + + + public function getTotalProductUseSet($set_id) + { + return $this->objComboSetModel->getTotalProductUseSet($set_id); + } + + + public function getListProductUseSet($set_id, $numPerPage) + { + return $this->objComboSetModel->getListProductUseSet($set_id, $numPerPage); + } + + + + + + public function getProductListInfoInConfig(array $category) { + $product_list_ids = []; + foreach ($category as $index => $_category_info) { + foreach ($_category_info['suggest_list'] as $_proindex => $_pro_info) { + $product_list_ids[] = $_pro_info['real_id']; + } + } + + return array_unique($product_list_ids); + } + + + public function buildConfig( $category, $product) { + + $group_category = []; + + foreach ($category as $category_index => $_category_info) { + $category_product = []; + foreach ($product[$category_index] as $product_index => $_product_info) { + //$_product_info['price'] = clean_price($_product_info['price']); + $category_product[] = $_product_info; + } + + $group_category[] = [ + "title" => $_category_info['title'], + //"type" => "category", + //"real_id" => $_category_info['real_id'], + //"select_type" => $_category_info['select_type'],//checkbox|radio + "suggest_list" => $category_product, + ]; + } + + return $group_category; + } + + + public function decomposeConfig($config) { + $tab = []; + $group = []; + $category = []; + $product = []; + + $group_index = 0; + $category_index = 0; + $product_index = 0; + + foreach ($config as $tab_index => $tab_info) { + //construct tab + $tab[$tab_index] = [ + 'title' => $tab_info['title'], + ]; + + //construct group + foreach ($tab_info['child'] as $child_group) { + $group_index += 1; + + $group[$tab_index][$group_index] = [ + 'title' => $child_group['title'], + ]; + + //construct category + foreach ($child_group['child'] as $child_category) { + $category_index += 1; + + $category[$group_index][$category_index] = [ + 'title' => $child_category['title'], + 'real_id' => $child_category['real_id'], + 'select_type' => $child_category['select_type'], + ]; + + //construct product + foreach ($child_category['suggest_list'] as $child_product) { + $product_index += 1; + + $product[$category_index][$product_index] = [ + 'title' => $child_product['title'], + 'real_id' => $child_product['real_id'], + 'is_default' => $child_product['is_default'], + ]; + } + } + } + } + + return [ + "tab" => $tab, + "group" => $group, + 'category' => $category, + 'product' => $product, + ]; + } + + + + +} diff --git a/inc/Hura8/Components/ComboSet/Model/ComboSetLanguageModel.php b/inc/Hura8/Components/ComboSet/Model/ComboSetLanguageModel.php new file mode 100644 index 0000000..c312404 --- /dev/null +++ b/inc/Hura8/Components/ComboSet/Model/ComboSetLanguageModel.php @@ -0,0 +1,17 @@ +db->runQuery( + " SELECT `set_id` FROM ".$this->tb_set_product." WHERE `product_id` = ? ", + ['d'], [$product_id] + ); + + $item_list = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + $item_list[] = $info['set_id']; + } + + return $item_list; + } + + + public function getTotalProductUseSet($set_id) + { + // search + $keyword = getRequest("q"); + if($keyword) { + $search = new ProductSearchModel(); + $match_result = $search->find($keyword); + $catCondition = (sizeof($match_result) > 0) ? " AND `product_id` IN (".join(",", $match_result).") " : " AND `product_id` = -1 "; + + $query = $this->db->runQuery(" + SELECT COUNT(product_id) AS total_product + FROM ".$this->tb_set_product." + WHERE `set_id` = ? " . $catCondition ." + ", ['d'], [$set_id]); + + if ($info = $this->db->fetchAssoc($query)) { + return $info['total_product']; + } + + return 0; + + } else { + $set_info = $this->getInfo($set_id); + + return $set_info['product_count']; + } + } + + + public function getListProductUseSet($set_id, $numPerPage) + { + $page = getPageId(); + + // search + $catCondition = ""; + $keyword = getRequest("q"); + if($keyword) { + $search = new ProductSearchModel(); + $match_result = $search->find($keyword); + $catCondition = (sizeof($match_result) > 0) ? " AND `product_id` IN (".join(",", $match_result).") " : " AND `product_id` = -1 "; + } + + $query = $this->db->runQuery(" + SELECT `product_id` + FROM ".$this->tb_set_product." + WHERE `set_id` = ? " . $catCondition ." + ORDER BY id desc + LIMIT ".($page - 1) * $numPerPage .", ".$numPerPage." + ", ['d'], [$set_id]); + + $item_list = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + $item_list[] = $info['product_id']; + } + + return $item_list; + } + + + protected function _buildQueryOrderBy(string $sort_by = "new") + { + $order_condition = ""; + + switch ($sort_by) { + case "ordering"; + $order_condition = " `ordering` desc "; + break; + case "old"; + $order_condition = " id asc "; + break; + case "last_show_time"; + $order_condition = " last_show_time ASC "; + break; + } + + return $order_condition; + } + + + protected function formatItemInfo(array $item_info) : array + { + $from_time = $item_info['from_time']; + $from_time_date = ($from_time > 0) ? date("d-m-Y", $from_time) : ''; + $from_time_minute = ($from_time > 0) ? date("H:i", $from_time) : "00:00"; + + $to_time = $item_info['to_time']; + $to_time_date = ($to_time > 0) ? date("d-m-Y", $to_time) : ''; + $to_time_minute = ($to_time > 0) ? date("H:i", $to_time) : "00:00"; + + $item_info['from_time_date'] = $from_time_date; + $item_info['from_time_minute'] = $from_time_minute; + $item_info['to_time_date'] = $to_time_date; + $item_info['to_time_minute'] = $to_time_minute; + + return $item_info; + } + + + + ///--------- + /// + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + + $catCondition = ""; + $bind_types = []; + $bind_values = []; + + if(isset($filter_condition["product_id"]) && $filter_condition["product_id"]){ + $catCondition .= " AND `id` IN ( SELECT `set_id` FROM ".$this->tb_set_product." WHERE `product_id` = ? ) "; + + $bind_types[] = 'd'; + $bind_values[] = $filter_condition['product_id']; + } + + return [$catCondition, $bind_types, $bind_values]; + } + + +} diff --git a/inc/Hura8/Components/ConfigGroup/AdminController/AConfigGroupController.php b/inc/Hura8/Components/ConfigGroup/AdminController/AConfigGroupController.php new file mode 100644 index 0000000..177ed0c --- /dev/null +++ b/inc/Hura8/Components/ConfigGroup/AdminController/AConfigGroupController.php @@ -0,0 +1,77 @@ +objConfigGroupModel->deleteAttribute($id, $group_id); + } + + + public function updateAttribute($id, $info) { + $this->objConfigGroupModel->updateAttribute($id, $info) ; + } + + + public function createAttribute($info) { + return $this->objConfigGroupModel->createAttribute($info) ; + } + + + public function createAttributeValue($info) { + $this->objConfigGroupModel->createAttributeValue($info); + } + + + public function deleteAttributeValue($id) { + $this->objConfigGroupModel->deleteAttributeValue($id); + } + + + public function updateAttributeValue($id, $info) { + $this->objConfigGroupModel->updateAttributeValue($id, $info); + } + + + public function createProduct($product_id, $group_id, array $attribute_config) { + $this->objConfigGroupModel->createProduct($product_id, $group_id, $attribute_config); + } + + + public function deleteProduct($product_id, $group_id) { + $this->objConfigGroupModel->deleteProduct($product_id, $group_id); + } + + + public function updateProduct($product_id, $group_id, array $attribute_config, $product_name_in_group = '') { + + return $this->objConfigGroupModel->updateProduct($product_id, $group_id, $attribute_config, $product_name_in_group); + } + + + public function getProductInGroup($group_id){ + return $this->objConfigGroupModel->getProductInGroup($group_id); + } + + + public function getGroupConfig($item_id) { + return $this->objConfigGroupModel->getGroupConfig($item_id); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + return true; + } + + +} diff --git a/inc/Hura8/Components/ConfigGroup/Controller/bConfigGroupController.php b/inc/Hura8/Components/ConfigGroup/Controller/bConfigGroupController.php new file mode 100644 index 0000000..3f5baff --- /dev/null +++ b/inc/Hura8/Components/ConfigGroup/Controller/bConfigGroupController.php @@ -0,0 +1,24 @@ +objConfigGroupModel = new ConfigGroupModel(); + parent::__construct($this->objConfigGroupModel); + } + + + +} diff --git a/inc/Hura8/Components/ConfigGroup/Model/ConfigGroupModel.php b/inc/Hura8/Components/ConfigGroup/Model/ConfigGroupModel.php new file mode 100644 index 0000000..e149db8 --- /dev/null +++ b/inc/Hura8/Components/ConfigGroup/Model/ConfigGroupModel.php @@ -0,0 +1,500 @@ +tb_config_group = $this->tb_entity; + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + protected function updateGroupAttributeCount($group_id) { + $this->db->runQuery( + "UPDATE `".$this->tb_config_group."` SET + `attribute_count` = ( SELECT COUNT(*) FROM `".$this->tb_config_group_attribute."` WHERE `group_id` = ? ) + WHERE `id` = ? LIMIT 1 ", + ['d', 'd'], [ $group_id, $group_id ] + ); + } + + + protected function updateGroupProductCount($group_id) { + $this->db->runQuery( + "UPDATE `".$this->tb_config_group."` SET + `item_count` = ( SELECT COUNT(*) FROM `".$this->tb_config_group_product."` WHERE `group_id` = ? ) + WHERE `id` = ? LIMIT 1 ", + ['d', 'd'], [ $group_id, $group_id ] + ); + } + + + //attribute value + public function deleteAttributeValue($att_value_id) { + + $attr_id = 0; + $query = $this->db->runQuery("SELECT `attr_id` FROM `".$this->tb_config_group_attribute_value."` WHERE `id` = ? LIMIT 1 ", ['d'], [ $att_value_id ]); + if ($info = $this->db->fetchAssoc($query)) { + $attr_id = $info['attr_id']; + } + + $group_id = $this->getGroupIdFromAttribute($attr_id); + + $this->db->runQuery("DELETE FROM `".$this->tb_config_group_attribute_value."` WHERE `id` = ? LIMIT 1 ", ['d'], [ $att_value_id ]); + + $this->resetProductConfigCache($group_id); + + $this->updateAttributeValueCount($attr_id); + } + + + protected function updateAttributeValueCount($attr_id) { + $this->db->runQuery( + "UPDATE `".$this->tb_config_group_attribute."` SET + `value_count` = ( SELECT COUNT(*) FROM `".$this->tb_config_group_attribute_value."` WHERE `attr_id` = ? ) + WHERE `id` = ? LIMIT 1 ", + ['d', 'd'], + [$attr_id, $attr_id ] + ); + } + + + public function updateAttributeValue($id, $info) { + + $updated_info = $info; + + $updated_info['last_update'] = CURRENT_TIME; + $updated_info['last_update_by'] = ADMIN_NAME; + + $this->db->update( + $this->tb_config_group_attribute_value, + $updated_info, + [ + "id" => $id, + ] + ); + + $group_id = $this->getGroupIdFromAttributeValue($id); + $this->resetProductConfigCache($group_id); + } + + + public function createAttributeValue($info) { + $updated_info = $info; + + $updated_info['create_time'] = CURRENT_TIME; + $updated_info['create_by'] = ADMIN_NAME; + $updated_info['last_update'] = CURRENT_TIME; + $updated_info['last_update_by'] = ADMIN_NAME; + + $this->db->insert($this->tb_config_group_attribute_value, $updated_info ); + + $group_id = $this->getGroupIdFromAttribute($info['attr_id']); + $this->resetProductConfigCache($group_id); + + $this->updateAttributeValueCount($info['attr_id']); + } + + //attribute + public function deleteAttribute($id, $group_id = 0) { + if(!$group_id) $group_id = $this->getGroupIdFromAttribute($id); + $this->db->runQuery("DELETE FROM `".$this->tb_config_group_attribute."` WHERE `id` = ? LIMIT 1 ", ['d'], [ $id ]); + $this->db->runQuery("DELETE FROM `".$this->tb_config_group_attribute_value."` WHERE `attr_id` = ? LIMIT 1 ", ['d'], [ $id ]); + + $this->updateGroupAttributeCount($group_id); + + //todo: update for product attribute_config ? + $this->resetProductConfigCache($group_id); + } + + public function updateAttribute($id, $info) { + + $updated_info = $info; + + $updated_info['last_update'] = CURRENT_TIME; + $updated_info['last_update_by'] = ADMIN_NAME; + + $this->db->update( + $this->tb_config_group_attribute, + $updated_info, + [ + 'id' => $id, + ] + ); + + $group_id = $this->getGroupIdFromAttribute($id); + $this->resetProductConfigCache($group_id); + } + + public function createAttribute($info) { + + $updated_info = $info; + + $updated_info['create_time'] = CURRENT_TIME; + $updated_info['create_by'] = ADMIN_NAME; + $updated_info['last_update'] = CURRENT_TIME; + $updated_info['last_update_by'] = ADMIN_NAME; + + $this->db->insert($this->tb_config_group_attribute, $updated_info ); + + $this->resetProductConfigCache($info['group_id']); + + $this->updateGroupAttributeCount($info['group_id']); + + return $this->db->get_insert_id(); + } + + private function getGroupIdFromAttributeValue($att_value_id) { + $attr_id = 0; + $query = $this->db->runQuery("SELECT `attr_id` FROM `".$this->tb_config_group_attribute_value."` WHERE `id` = ? LIMIT 1 ", ['d'], [ $att_value_id ]); + if ($info = $this->db->fetchAssoc($query)) { + $attr_id = $info['attr_id']; + } + + return $this->getGroupIdFromAttribute($attr_id); + } + + private function getGroupIdFromAttribute($attr_id) { + $group_id = 0; + $query = $this->db->runQuery("SELECT `group_id` FROM `".$this->tb_config_group_attribute."` WHERE `id` = ? LIMIT 1 ", ['d'], [ $attr_id ]); + if ($info = $this->db->fetchAssoc($query)) { + $group_id = $info['group_id']; + } + + return $group_id; + } + + //create or update product in a group + public function createProduct($product_id, $group_id, array $attribute_config) { + if($this->isProductInGroup($product_id, $group_id)) { + + $this->updateProduct($product_id, $group_id, $attribute_config); + + }else { + + $updated_info = [ + "product_id" => $product_id, + "group_id" => $group_id, + "attribute_config" => json_encode($attribute_config), + + "create_time" => CURRENT_TIME, + "create_by" => ADMIN_NAME, + "last_update" => CURRENT_TIME, + "last_update_by" => ADMIN_NAME, + ]; + + $this->db->insert($this->tb_config_group_product, $updated_info ); + + $this->updateGroupProductCount($group_id); + + } + + $this->resetProductConfigCache($group_id); + } + + public function deleteProduct($product_id, $group_id) { + $this->db->runQuery( + "DELETE FROM `".$this->tb_config_group_product."` WHERE `product_id` = ? AND `group_id` = ? LIMIT 1 ", + ['d', 'd'], [ $product_id, $group_id ] + ); + + $this->deleteProductConfigCache($product_id); + + $this->updateGroupProductCount($group_id); + } + + public function updateProduct($product_id, $group_id, array $attribute_config, $product_name_in_group = '') { + + $this->db->update( + $this->tb_config_group_product, + [ + "attribute_config" => json_encode($attribute_config), + "product_name_in_group" => substr($product_name_in_group, 0, 140), + "last_update" => CURRENT_TIME, + "last_update_by" => ADMIN_NAME, + ], + [ + "product_id" => $product_id, + "group_id" => $group_id, + ] + ); + + $this->resetProductConfigCache($group_id); + + return true; + } + + + //we want to reset all caches for products in a group + //we need to do this when we make changes to group's attributes, or create new products/ update product in group + protected function resetProductConfigCache($group_id) { + $query = $this->db->runQuery("SELECT `product_id` FROM `".$this->tb_config_group_product."` WHERE `group_id` = ? ", ['d'], [$group_id]) ; + $product_list = array(); + foreach( $this->db->fetchAll($query) as $item ){ + $product_list[] = $item['product_id']; + } + + if(sizeof($product_list)) { + $this->db->query("UPDATE `".$this->tb_config_group_product_cache."` SET + `value` = NULL + WHERE `product_id` IN (".join(",", $product_list).") ") ; + } + } + + + protected function deleteProductConfigCache($product_id) { + $this->db->runQuery("DELETE FROM `".$this->tb_config_group_product_cache."` WHERE `product_id` = ? LIMIT 1 ", ['d'], [$product_id]) ; + } + + + public function saveProductConfigCache($product_id, $value) { + + $query = $this->db->runQuery( + "SELECT `product_id` FROM `".$this->tb_config_group_product_cache."` WHERE `product_id` = ? LIMIT 1 ", + ['d'], [$product_id] + ) ; + + if($this->db->fetchAssoc($query)){ + $this->db->runQuery( + "UPDATE `".$this->tb_config_group_product_cache."` SET + `value` = '".$this->db->escape(json_encode($value))."' + WHERE `product_id` = ? LIMIT 1 ", + ['d'], [$product_id] + ) ; + }else{ + $this->db->runQuery("INSERT INTO `".$this->tb_config_group_product_cache."` (`product_id`, `value`) + VALUES ('". (int) $product_id."', '".$this->db->escape(json_encode($value))."') ") ; + } + } + + + public function getProductConfigCache($product_id) { + $query = $this->db->runQuery("SELECT `value` FROM `".$this->tb_config_group_product_cache."` WHERE `product_id` = ? LIMIT 1 ", ['d'], [$product_id]) ; + if($item_info = $this->db->fetchAssoc($query)){ + return ($item_info['value']) ? \json_decode($item_info['value'], true) : false; + } + + return false; + } + + + //get all group config + public function getGroupConfig($group_id) { + + $query = $this->db->runQuery( + "SELECT + a.id AS attribute_id , + a.name AS attribute_name , + a.ordering AS attr_ordering , + v.id AS value_id , + v.name AS value_name , + v.image AS image , + v.color_code AS color , + v.ordering , + v.description AS description + FROM `".$this->tb_config_group_attribute."` a + LEFT JOIN `".$this->tb_config_group_attribute_value."` v ON a.id = v.attr_id + WHERE a.group_id = ? + ORDER BY attr_ordering DESC, `ordering` DESC + ", + ['d'], [$group_id] + ); + + $group_attribute = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + if(!isset($group_attribute[$info['attribute_id']])) { + $group_attribute[$info['attribute_id']] = array( + 'id' => $info['attribute_id'], + 'name' => $info['attribute_name'], + 'ordering' => $info['attr_ordering'], + 'list' => array(), + ); + } + + if($info['value_id']) { + $group_attribute[$info['attribute_id']]['list'][] = array( + 'id' => $info['value_id'], + 'name' => $info['value_name'], + 'image' => $info['image'], + 'color' => $info['color'], + 'ordering' => $info['ordering'], + 'description' => $info['description'], + ); + } + } + + return $group_attribute; + } + + + public function getProductConfigGroupId($product_id) { + $query = $this->db->runQuery( + "SELECT `group_id` FROM `".$this->tb_config_group_product."` WHERE `product_id` = ? LIMIT 1 ", + ['d'], [$product_id] + ) ; + + if($item_info = $this->db->fetchAssoc($query)){ + return $item_info['group_id']; + } + + return 0; + } + + + public function getProductInGroup($group_id){ + $query = $this->db->runQuery( + "SELECT `product_id`, `product_name_in_group`, `attribute_config` + FROM `".$this->tb_config_group_product."` + WHERE `group_id` = ? + ", + ['d'], [$group_id] + ); + + $product_list = array(); + $product_ids = array(); + + foreach ( $this->db->fetchAll($query) as $info ) { + $product_ids[] = $info['product_id']; + + $product_list[$info['product_id']] = array( + "id" => $info['product_id'], + "name" => "", + "product_name_in_group" => $info['product_name_in_group'], + "attribute" => \json_decode($info['attribute_config'], true), + "url" => "", + "sku" => "", + "price" => 0, + "image" => "", + "status" => "", + ); + } + + //find product urls + if(sizeof($product_ids)) { + $objProductModel = new ProductModel(); + $product_list_info = $objProductModel->getListByIds($product_ids); + + // debug_var($product_list_info); + // update $product_list + foreach ($product_list as $_pro_id => $_info) { + $_pro_info = $product_list_info[$_pro_id] ?? null; + if($_pro_info) { + $product_list[$_pro_id]['name'] = $_pro_info['title']; + $product_list[$_pro_id]['price'] = $_pro_info['price']; + $product_list[$_pro_id]['sku'] = $_pro_info['sku']; + $product_list[$_pro_id]['image'] = $_pro_info['thumbnail']; + $product_list[$_pro_id]['url'] = $_pro_info['request_path']; + $product_list[$_pro_id]['status'] = $_pro_info['status']; + } + } + } + + return $product_list; + } + + + protected function isProductInGroup($product_id, $group_id) { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_config_group_product."` WHERE `product_id` = ? AND `group_id` = ? LIMIT 1 ", + ['d', 'd'], + [$product_id, $group_id] + ) ; + + return ($this->db->fetchAssoc($query)); + } + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + /*$condition = array( + "q" => "", + "numPerPage" => 20, + "order_by" => '', + );*/ + + $catCondition = ""; + + return [ + $catCondition, + [], + [] + ]; + } + + protected function beforeCreateItem(array $input_info): AppResponse + { + $info = $input_info; + + $info['create_time'] = CURRENT_TIME; + $info['create_by'] = ADMIN_NAME; + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + protected function afterCreateItem($new_item_id, $new_item_info) + { + // TODO: Implement afterCreateItem() method. + } + + protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info):AppResponse + { + $info = $new_input_info; + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) + { + // TODO: Implement afterUpdateItem() method. + } + + protected function beforeDeleteItem($item_id, $item_info):AppResponse + { + return new AppResponse('ok' ); + } + + protected function afterDeleteItem($item_id, $item_info) + { + + $query = $this->db->runQuery("SELECT `id` FROM `".$this->tb_config_group_attribute."` WHERE `group_id` = ? ", ['d'], [$item_id]); + + foreach ( $this->db->fetchAll($query) as $info ) { + $this->deleteAttribute($info['id'], $item_id); + } + + $this->db->runQuery( + "DELETE FROM `".$this->tb_config_group_product_cache."` WHERE `product_id` IN (SELECT product_id FROM config_group_product WHERE `group_id` = ? ) ", + ['d'], [$item_id] + ) ; + + $this->db->runQuery("DELETE FROM `".$this->tb_config_group_product."` WHERE `group_id` = ? ", ['d'], [$item_id]) ; + } +} diff --git a/inc/Hura8/Components/Customer/AdminController/ACustomerController.php b/inc/Hura8/Components/Customer/AdminController/ACustomerController.php new file mode 100644 index 0000000..f01f7f6 --- /dev/null +++ b/inc/Hura8/Components/Customer/AdminController/ACustomerController.php @@ -0,0 +1,23 @@ +objCustomerGroupModel = new CustomerGroupModel(); + parent::__construct($this->objCustomerGroupModel); + } + + + public function getAllGroup() { + return $this->objCustomerGroupModel->getList(["numPerPage" => 1000]); + } + + + public function removeCustomer($customer_id, $group_id) + { + return $this->objCustomerGroupModel->removeCustomer($customer_id, $group_id); + } + + + public function addCustomer($customer_id, $group_id) + { + return $this->objCustomerGroupModel->addCustomer($customer_id, $group_id); + } + + + public function getTotalCustomer($group_id, array $condition = []) + { + return $this->objCustomerGroupModel->getTotalCustomer($group_id, $condition); + } + + + public function getListCustomer($group_id, array $condition = []) { + + $objProvinceController = new AProvinceController(); + + return array_map(function ($item) use ($objProvinceController){ + + $item['province_name'] = $objProvinceController->getProvinceName($item['province']); + + return $item; + + }, $this->objCustomerGroupModel->getListCustomer($group_id, $condition)); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // TODO: Implement deleteFileBeforeDeleteItem() method. + return true; + } +} diff --git a/inc/Hura8/Components/Customer/AdminController/ACustomerLoyaltyController.php b/inc/Hura8/Components/Customer/AdminController/ACustomerLoyaltyController.php new file mode 100644 index 0000000..d3eb87c --- /dev/null +++ b/inc/Hura8/Components/Customer/AdminController/ACustomerLoyaltyController.php @@ -0,0 +1,9 @@ +objCustomerModel = new CustomerModel(); + $this->objProvinceController = new AProvinceController(); + parent::__construct($this->objCustomerModel); + } + + + public function formatItemInList(array $item_info) + { + return $this->formatItemInfo($item_info); + } + + + public function formatItemInfo(array $item_info) + { + $info = $item_info; + $info['province_name'] = $this->objProvinceController->getProvinceName($item_info['province']); + + return $info; + } + +} diff --git a/inc/Hura8/Components/Customer/Controller/bCustomerLoyaltyController.php b/inc/Hura8/Components/Customer/Controller/bCustomerLoyaltyController.php new file mode 100644 index 0000000..8ca17ce --- /dev/null +++ b/inc/Hura8/Components/Customer/Controller/bCustomerLoyaltyController.php @@ -0,0 +1,206 @@ +objCustomerLoyaltyModel = new CustomerLoyaltyModel(); + + // import settings + //$new_info_file = "../config/build/customer_point.php" ; + //$config_file = ROOT_DIR . "/config/build/customer_point.php"; + if(@file_exists($this->point_setting_config_file)) { + $this->point_setting = include $this->point_setting_config_file; + } + + // customer level based on point gain + if( defined("ENABLE_CUSTOMER_POINT") && ENABLE_CUSTOMER_POINT ) { + if(@file_exists($this->level_setting_config_file)) { + $this->level_setting = include $this->level_setting_config_file; + } + } + + // default is point + $this->level_by = (defined('CHANGE_CUSTOMER_LEVEL_BY')) ? CHANGE_CUSTOMER_LEVEL_BY : 'point'; + + $this->objUserLoyaltyPointCalculation = new UserLoyaltyPointCalculation($this->point_setting); + + } + + + public function getPointSettingConfigFile() { + return $this->point_setting_config_file; + } + + public function getPointSetting() { + return $this->point_setting; + } + + public function getLevelSetting(){ + return $this->level_setting; + } + + // show estimate cart point + public function getEstimateCartPoint($cart_value){ + $conversion_rate = (isset($this->point_setting['reward']['buy']['rate'])) ? $this->point_setting['reward']['buy']['rate'] : 0; + return ($conversion_rate) ? round($cart_value / $conversion_rate) : 0; + } + + public function getUserPoint($user_id, array $condition, $return_type) + { + return $this->objCustomerLoyaltyModel->getUserPoint($user_id, $condition, $return_type); + } + + // remove rewarded point i.e. reward for successfull order and now order is marked canceled + public function reclaimRewardedPoint($user_id, $activity_type_tracker){ + // todo: + } + + public function usePoint($user_id, $use_point, $activity_type, $activity_type_tracker, $reason = '', $point_args = ['order_value' => 0]){ + // no user or no config + if(!$user_id || !ENABLE_CUSTOMER_POINT) return false; + + $result = $this->objUserLoyaltyPointCalculation->calculateUsePoint($user_id, $use_point, $activity_type, $activity_type_tracker, $point_args); + + $this->pointOp('use', $user_id, $result['use_point'], $activity_type, $activity_type_tracker, $reason); + + return $result; + } + + + public function rewardPoint($user_id, $activity_type, $activity_type_tracker, $reason = '', $point_args = ['order_id' => 0]){ + // no user or no config + if(!$user_id || !ENABLE_CUSTOMER_POINT) return false; + + $point = $this->objUserLoyaltyPointCalculation->calculateRewardPoint($user_id, $activity_type, $activity_type_tracker, $point_args); + + $this->pointOp('reward', $user_id, $point, $activity_type, $activity_type_tracker, $reason); + + return $point; + } + + // $operation: reward|use + // $change_point: positive (reward) or nagative (use) + protected function pointOp($operation, $user_id, $change_point, $activity_type, $activity_type_tracker, $reason = '') { + + if(!$change_point) return false; + + $reason_prefix = ($operation == 'use') ? 'Sử dụng' : 'Thưởng'; + if($activity_type == 'return') $reason_prefix = 'Hoàn lại'; + $full_reason = join(" ", [$reason_prefix, $change_point, static::$POINT_NAME, ":", $reason]); + + if($operation == 'use') $change_point = -1 * $change_point; + + // security: hash the row to avoid editing point directly in the database + $hash_value = sha1(join(".", [ + $operation, + $user_id, + $change_point, + $activity_type, + $activity_type_tracker, + CURRENT_TIME, + 'ass@ss' + ])); + + $new_id = $this->db->insert( + $this->tb_point , + [ + 'customer_id' => $user_id , + 'activity_type' => $activity_type, + 'activity_type_tracker' => $activity_type_tracker , + 'operation' => $operation, + 'point' => $change_point, + 'create_time' => CURRENT_TIME, + 'reason' => substr($full_reason, 0, 200), + 'referer_url' => substr(REFERER_URL, 0, 150) , + 'hash_value' => $hash_value , + ] + ); + + //update user reward balance + if($new_id) { + $this->updateStat($user_id, $change_point); + } + + return $new_id; + } + + protected function updateStat($user_id, $changed_point, $changed_order_value=0) { + return $this->objCustomerLoyaltyModel->updateStat($user_id, $changed_point, $changed_order_value); + } + + private function calculateLevelByPoint($point) { + //if the point in between -> return the lowest level + $all_level = array_keys($this->level_setting); + + foreach ( $all_level as $level) { + $next_level = $level + 1; + if(!in_array($next_level, $all_level)) $next_level = 0; + + if($next_level) { + if( $point >= $this->level_setting[$level]["point_require"] + && $point < $this->level_setting[$next_level]["point_require"] ) { + return $level; + } + }else{ + if($point >= $this->level_setting[$level]["point_require"]) { + return $level; + } + } + } + + return 0; + } + + private function calculateLevelByOrderValue($aggregate_purchase_value = 0) { + + //if the point in between -> return the lowest level + $all_level = array_keys($this->level_setting); + + //tinh hang thanh vien theo so tien tich luy + foreach ( $all_level as $level ) { + $next_level = $level + 1; + if(!in_array($next_level, $all_level)) $next_level = 0; + + if($next_level) { + if( $aggregate_purchase_value >= $this->level_setting[$level]["total_order_value"] && + $aggregate_purchase_value < $this->level_setting[$next_level]["total_order_value"] + ) { + return $level; + } + }else{ + if( $aggregate_purchase_value >= $this->level_setting[$level]["total_order_value"]) { + return $level; + } + } + } + + return 0; + } + + +} diff --git a/inc/Hura8/Components/Customer/Model/CustomerAuthModel.php b/inc/Hura8/Components/Customer/Model/CustomerAuthModel.php new file mode 100644 index 0000000..3360bad --- /dev/null +++ b/inc/Hura8/Components/Customer/Model/CustomerAuthModel.php @@ -0,0 +1,79 @@ +tb_customer_login, $this->tb_customer_access_code); + } + + + public function getLoginListByIds(array $staff_ids) { + if(!sizeof($staff_ids)) { + return []; + } + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($staff_ids, 'int'); + + $bind_values = $staff_ids; + + $query = $this->db->runQuery( + "SELECT `user_id`, `last_login_time`, `last_login_ip`, `last_login_device`, `last_login_browser` + FROM ".$this->tb_customer_login." + WHERE `user_id` IN (".$parameterized_ids.") ", + $bind_types, + $bind_values + ); + + $item_list = []; + foreach ($this->db->fetchAll($query) as $item) { + $item_list[$item['user_id']] = $item; + } + + return $item_list; + } + + + public function getLoginLog(array $conditions = []) { + $bind_types = []; + $bind_values = []; + + $query = $this->db->runQuery( + "SELECT * FROM ".$this->tb_customer_login_log." WHERE 1 ORDER BY `id` DESC LIMIT 100 ", + $bind_types, + $bind_values + ); + + return $this->db->fetchAll($query) ; + } + + + /** + * @param $email + * @param string $login_status ok or error + * @param string $login_msg + */ + public function logLogin($email, $login_status, $login_msg) { + $this->db->insert( + $this->tb_customer_login_log, + [ + "email" => substr($email, 0, 45), + "login_status" => $login_status, + "login_msg" => substr($login_msg, 0, 45), + "ip_address" => substr(USER_IP, 0, 45), + "user_agent" => substr(USER_AGENT, 0, 99), + "create_time" => CURRENT_TIME, + ] + ); + } + +} diff --git a/inc/Hura8/Components/Customer/Model/CustomerGroupModel.php b/inc/Hura8/Components/Customer/Model/CustomerGroupModel.php new file mode 100644 index 0000000..ee243fd --- /dev/null +++ b/inc/Hura8/Components/Customer/Model/CustomerGroupModel.php @@ -0,0 +1,213 @@ +db->runQuery( + "UPDATE `".$this->tb_entity."` SET + `customer_count` = (SELECT COUNT(*) AS total FROM `".$this->tb_customer_per_group."` WHERE `group_id` = ? ) + WHERE `id` = ? LIMIT 1 + ", + ['d', 'd'], [$group_id, $group_id] + ); + } + + + public function getTotalCustomer($group_id, array $condition = []) + { + $query = $this->db->runQuery( + " SELECT COUNT(*) as total FROM `".$this->tb_customer_per_group."` WHERE `group_id` = ? ", + ['d'], [$group_id] + ); + + $total = 0; + if ($rs = $this->db->fetchAssoc($query)) { + $total = $rs['total']; + } + + return $total; + } + + + public function getListCustomer($group_id, array $condition = []) + { + $numPerPage = (isset($condition['numPerPage']) && $condition['numPerPage'] > 0 ) ? intval($condition['numPerPage']) : 20 ; + $page = (isset($condition['page']) && $condition['page'] > 0 ) ? intval($condition['page']) : 1 ; + $order_by = " `id` DESC"; + + $query = $this->db->runQuery( + "SELECT `customer_id` FROM ".$this->tb_customer_per_group." WHERE `group_id` = ? + ORDER BY ".$order_by." + LIMIT ".(($page-1) * $numPerPage).", ".$numPerPage , + ['d'], [$group_id] + ) ; + + $item_list_ids = array_map(function ($item){ return $item['customer_id'];}, $this->db->fetchAll($query)); + + $objCustomerModel = new CustomerModel(); + $list_info = $objCustomerModel->getListByIds($item_list_ids); + + // final list + $final_list = []; + foreach ($item_list_ids as $_id) { + $final_list[] = $list_info[$_id] ?? null; + } + + return $final_list; + } + + + public function removeCustomerFromAllGroup($customer_id) + { + $this->db->runQuery( + "DELETE FROM `".$this->tb_customer_per_group."` WHERE `customer_id` = ?", + ['d'], [$customer_id] + ); + + return true; + } + + + public function removeCustomer($customer_id, $group_id) + { + $this->db->runQuery( + "DELETE FROM `".$this->tb_customer_per_group."` WHERE `group_id` =? AND `customer_id` = ? LIMIT 1 ", + ['d', 'd'], + [$group_id, $customer_id] + ); + + $this->updateItemCount($group_id); + + return true; + } + + + public function addCustomer($customer_id, $group_id) + { + $query = $this->db->runQuery( + " SELECT * FROM `".$this->tb_customer_per_group."` WHERE `group_id` = ? AND `customer_id` = ? LIMIT 1 ", + ['d', 'd'], + [$group_id, $customer_id] + ); + + if ($this->db->fetchAssoc($query)) { + return false; + } + + $this->db->insert( + $this->tb_customer_per_group, + [ + "group_id" => $group_id, + "customer_id" => $customer_id, + ] + ); + + $this->updateItemCount($group_id); + + return true; + } + + + protected function _buildQueryConditionExtend(array $filter_condition): ?array + { + /*$condition = array( + "q" => "", + "status" => 0, + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + $info = $input_info; + + if(!$info['group_code']) $info['group_code'] = $info['title']; + $info['group_code'] = $this->createUniqueCode(0, $info['group_code']); + + $info['create_time'] = CURRENT_TIME; + $info['create_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + + protected function afterCreateItem($new_item_id, $new_item_info) + { + + } + + protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info): AppResponse + { + $info = $new_input_info; + + if(isset($info['group_code'])) { + if(!$info['group_code']) $info['group_code'] = $info['title']; + $info['group_code'] = $this->createUniqueCode($item_id, $info['group_code']); + } + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + + protected function createUniqueCode($current_item_id, $wanted_code){ + + $clean_code = UrlManagerController::create_url_index($wanted_code); + + //if exist and belong other id, create a new one + $query = $this->db->runQuery("SELECT `id` FROM `".$this->tb_entity."` WHERE `group_code` = ? LIMIT 1 ", ['s'], [$clean_code]) ; + if($info = $this->db->fetchAssoc($query)){ + if($info['id'] != $current_item_id) { + $new_code = $clean_code."-".IDGenerator::createStringId(3); + return $this->createUniqueCode($current_item_id, $new_code); + } + } + + return $clean_code; + } + + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) + { + + } + + protected function beforeDeleteItem($item_id, $item_info) : AppResponse + { + return new AppResponse('ok'); + } + + protected function afterDeleteItem($item_id, $item_info) + { + + } + + protected function extendedFilterOptions(): array + { + return []; + } +} diff --git a/inc/Hura8/Components/Customer/Model/CustomerLoyaltyModel.php b/inc/Hura8/Components/Customer/Model/CustomerLoyaltyModel.php new file mode 100644 index 0000000..a10b3ea --- /dev/null +++ b/inc/Hura8/Components/Customer/Model/CustomerLoyaltyModel.php @@ -0,0 +1,268 @@ +db = get_db("", ENABLE_DB_DEBUG); + + // import settings + //$new_info_file = "../config/build/customer_point.php" ; + //$config_file = ROOT_DIR . "/config/build/customer_point.php"; + if(@file_exists($this->point_setting_config_file)) { + $this->point_setting = include $this->point_setting_config_file; + } + + // customer level based on point gain + if( defined("ENABLE_CUSTOMER_POINT") && ENABLE_CUSTOMER_POINT ) { + if(@file_exists($this->level_setting_config_file)) { + $this->level_setting = include $this->level_setting_config_file; + } + } + + // default is point + $this->level_by = (defined('CHANGE_CUSTOMER_LEVEL_BY')) ? CHANGE_CUSTOMER_LEVEL_BY : 'point'; + + $this->objUserLoyaltyPointCalculation = new UserLoyaltyPointCalculation($this->point_setting); + } + + + public function getPointSettingConfigFile() { + return $this->point_setting_config_file; + } + + public function getPointSetting() { + return $this->point_setting; + } + + public function getLevelSetting(){ + return $this->level_setting; + } + + // show estimate cart point + public function getEstimateCartPoint($cart_value){ + $conversion_rate = (isset($this->point_setting['reward']['buy']['rate'])) ? $this->point_setting['reward']['buy']['rate'] : 0; + return ($conversion_rate) ? round($cart_value / $conversion_rate) : 0; + } + + public function getUserPoint($user_id, array $condition, $return_type) + { + + if($return_type == "total") { + //Lay tong so + $query = $this->db->runQuery("SELECT COUNT(*) AS total FROM `". $this->tb_point ."` WHERE `customer_id` = ? " , ['d'], [$user_id]); + if($resultTotal = $this->db->fetchAssoc($query)){ + return $resultTotal['total']; + } + return 0; + + } else { + + $numPerPage = (isset($condition['numPerPage'])) ? intval($condition['numPerPage']) : 50; + $page = getPageId(); + + $query = $this->db->runQuery(" + SELECT * FROM `". $this->tb_point ."` + WHERE `customer_id` = ? + ORDER BY id DESC + LIMIT ".($page - 1) * $numPerPage .", ".$numPerPage." " , ['d'], [$user_id]); + + $result = array(); + $i = ($page - 1) * $numPerPage; + foreach ( $this->db->fetchAll($query) as $rs){ + $i++; + $rs['counter'] = $i; + $rs['activity_type_name'] = (isset($this->point_setting[$rs['operation']][$rs['activity_type']])) ? $this->point_setting[$rs['operation']][$rs['activity_type']]['name'] : '--'; + $result[] = $rs; + } + + return $result; + } + + } + + + public function usePoint($user_id, $use_point, $activity_type, $activity_type_tracker, $reason = '', $point_args = ['order_value' => 0]){ + // no user or no config + if(!$user_id || !ENABLE_CUSTOMER_POINT) return false; + + $result = $this->objUserLoyaltyPointCalculation->calculateUsePoint($user_id, $use_point, $activity_type, $activity_type_tracker, $point_args); + + $this->pointOp('use', $user_id, $result['use_point'], $activity_type, $activity_type_tracker, $reason); + + return $result; + } + + + public function rewardPoint($user_id, $activity_type, $activity_type_tracker, $reason = '', $point_args = ['order_id' => 0]){ + // no user or no config + if(!$user_id || !ENABLE_CUSTOMER_POINT) return false; + + $point = $this->objUserLoyaltyPointCalculation->calculateRewardPoint($user_id, $activity_type, $activity_type_tracker, $point_args); + + $this->pointOp('reward', $user_id, $point, $activity_type, $activity_type_tracker, $reason); + + return $point; + } + + + // $operation: reward|use + // $change_point: positive (reward) or nagative (use) + protected function pointOp($operation, $user_id, $change_point, $activity_type, $activity_type_tracker, $reason = '') { + + if(!$change_point) return false; + + $reason_prefix = ($operation == 'use') ? 'Sử dụng' : 'Thưởng'; + if($activity_type == 'return') $reason_prefix = 'Hoàn lại'; + $full_reason = join(" ", [$reason_prefix, $change_point, static::$POINT_NAME, ":", $reason]); + + if($operation == 'use') $change_point = -1 * $change_point; + + // security: hash the row to avoid editing point directly in the database + $hash_value = sha1(join(".", [ + $operation, + $user_id, + $change_point, + $activity_type, + $activity_type_tracker, + CURRENT_TIME, + 'ass@ss' + ])); + + $new_id = $this->db->insert( + $this->tb_point , + [ + 'customer_id' => $user_id , + 'activity_type' => $activity_type, + 'activity_type_tracker' => $activity_type_tracker , + 'operation' => $operation, + 'point' => $change_point, + 'create_time' => CURRENT_TIME, + 'reason' => substr($full_reason, 0, 200), + 'referer_url' => substr(REFERER_URL, 0, 150) , + 'hash_value' => $hash_value , + ] + ); + + //update user reward balance + if($new_id) { + $this->updateStat($user_id, $change_point); + } + + return $new_id; + } + + + public function updateStat($user_id, $changed_point, $changed_order_value=0) { + $user_id = intval($user_id); + + $query = $this->db->runQuery("SELECT + `loyalty_point`, + `loyalty_level`, + `total_value_success` + FROM ".$this->tb_customer." + WHERE `id` = ? + LIMIT 1 " , ['d'], [$user_id]); + + if($current = $this->db->fetchAssoc($query)){ + $new_point = $current['loyalty_point'] + $changed_point; + $new_purchase_value = $current['total_value_success'] + $changed_order_value; + + $level = $current['loyalty_level']; + if($this->level_by == 'point' && $changed_point != 0) $level = $this->calculateLevelByPoint($new_point); + else if($changed_order_value) $level = $this->calculateLevelByOrderValue($new_purchase_value); + + $this->db->update( + $this->tb_customer , + [ + 'loyalty_point' => $new_point, + 'loyalty_level' => $level, + 'total_value_success' => $new_purchase_value, + ], + [ + 'id' => $user_id, + ], + 1 + ); + } + + return true; + } + + + private function calculateLevelByPoint($point) { + //if the point in between -> return the lowest level + $all_level = array_keys($this->level_setting); + + foreach ( $all_level as $level) { + $next_level = $level + 1; + if(!in_array($next_level, $all_level)) $next_level = 0; + + if($next_level) { + if( $point >= $this->level_setting[$level]["point_require"] + && $point < $this->level_setting[$next_level]["point_require"] ) { + return $level; + } + }else{ + if($point >= $this->level_setting[$level]["point_require"]) { + return $level; + } + } + } + + return 0; + } + + private function calculateLevelByOrderValue($aggregate_purchase_value = 0) { + + //if the point in between -> return the lowest level + $all_level = array_keys($this->level_setting); + + //tinh hang thanh vien theo so tien tich luy + foreach ( $all_level as $level ) { + $next_level = $level + 1; + if(!in_array($next_level, $all_level)) $next_level = 0; + + if($next_level) { + if( $aggregate_purchase_value >= $this->level_setting[$level]["total_order_value"] && + $aggregate_purchase_value < $this->level_setting[$next_level]["total_order_value"] + ) { + return $level; + } + }else{ + if( $aggregate_purchase_value >= $this->level_setting[$level]["total_order_value"]) { + return $level; + } + } + } + + return 0; + } +} diff --git a/inc/Hura8/Components/Customer/Model/CustomerModel.php b/inc/Hura8/Components/Customer/Model/CustomerModel.php new file mode 100644 index 0000000..3257c7c --- /dev/null +++ b/inc/Hura8/Components/Customer/Model/CustomerModel.php @@ -0,0 +1,128 @@ + "Chưa đăng ký", + "register" => "Đăng ký thành viên", + ]; + + /* @var iSearch $objSearchModel */ + protected $objSearchModel; + + public function __construct() { + $this->objSearchModel = new CustomerSearchModel(); + + parent::__construct( + EntityType::CUSTOMER, + "", + $this->objSearchModel + ); + } + + + protected function extendedFilterOptions() : array + { + return [ + 'province' => 0, + 'user_type' => '', + ]; + } + + + public function getInfoByCRMCode($code) + { + $query = $this->db->runQuery("SELECT * FROM `".$this->tb_entity."` WHERE `crm_code` = ? LIMIT 1 ", ['s'], [$code]) ; + if( $item_info = $this->db->fetchAssoc($query)){ + return $this->formatItemInfo($item_info); + } + + return false; + } + + + public function getInfoByEmail($email, $user_type='register') + { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_entity."` WHERE `email` = ? AND `type` = ? LIMIT 1 ", + ['s', 's'], [$email, $user_type] + ) ; + + if( $item_info = $this->db->fetchAssoc($query)){ + return $this->formatItemInfo($item_info); + } + + return null; + } + + + public function getInfoByVerifiedEmail($email) + { + $info = $this->getInfoByEmail($email); + if($info && $info['is_email_verify']) { + return $info; + } + + return null; + } + + + protected function _buildQueryConditionExtend(array $filter_condition): ?array + { + /*$condition = array( + "user_type" => '' + "q" => "", + "status" => 0, + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + if(isset($filter_condition["province"]) && $filter_condition["province"]) { + $catCondition[] = " AND `province` = ? "; + $bind_types[] = 'd'; + $bind_values[] = $filter_condition["province"]; + } + + // user_type + if(isset($filter_condition["user_type"]) && array_key_exists($filter_condition["user_type"], $this->user_types) ){ + $catCondition[] = " AND `type` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["user_type"]; + } + + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function formatItemInfo(array $item_info): array + { + if($item_info["birth_day"] && $item_info["birth_year"]) { + $item_info["birth_day"] = $item_info["birth_day"]."-".$item_info["birth_year"]; + } + + return $item_info; + } + + + protected function createWebCRMCode($input_code = '') { + return $input_code ?: "Web-".IDGenerator::createStringId(8); + } + + +} diff --git a/inc/Hura8/Components/Customer/Model/CustomerSearchModel.php b/inc/Hura8/Components/Customer/Model/CustomerSearchModel.php new file mode 100644 index 0000000..42e9bcd --- /dev/null +++ b/inc/Hura8/Components/Customer/Model/CustomerSearchModel.php @@ -0,0 +1,34 @@ + "tb_customer.type", + "province" => "tb_customer.province", + "is_email_verify" => "tb_customer.is_email_verify", + "total_value" => "tb_customer.total_value", + ]; + + private $fulltext_fields = [ + "keywords" => ["tb_customer.name", "tb_customer.crm_code", "tb_customer.email","tb_customer.mobile",], + ]; + + + public function __construct() + { + parent::__construct( + "tb_customer", + $this->fulltext_fields, + $this->filter_fields + ); + } + +} diff --git a/inc/Hura8/Components/Deal/AdminController/ADealCollectionController.php b/inc/Hura8/Components/Deal/AdminController/ADealCollectionController.php new file mode 100644 index 0000000..6cc7b73 --- /dev/null +++ b/inc/Hura8/Components/Deal/AdminController/ADealCollectionController.php @@ -0,0 +1,51 @@ +objDealCollectionModel = new DealCollectionModel(); + parent::__construct($this->objDealCollectionModel); + } + + public function getAllProductIdInCollection($collection_id) { + return $this->objDealCollectionModel->getAllProductIdInCollection($collection_id); + } + + public function getAllDealIdInCollection($collection_id) { + return $this->objDealCollectionModel->getAllDealIdInCollection($collection_id); + } + + public function updateAllDealInCollection($collection_id, $price, $time){ + return $this->objDealCollectionModel->updateAllDealInCollection($collection_id, $price, $time); + } + + public function addProductToCollection($product_id, $collection_id){ + return $this->objDealCollectionModel->addProductToCollection($product_id, $collection_id); + } + + public function removeFromCollection($deal_id, $collection_id){ + $this->objDealCollectionModel->removeFromCollection($deal_id, $collection_id); + } + + public function addToCollection($deal_id, $collection_id){ + $this->objDealCollectionModel->addToCollection($deal_id, $collection_id); + } + + public function updateCollectionView($id){ + $this->objDealCollectionModel->updateCollectionView($id); + } + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + return true; + } +} diff --git a/inc/Hura8/Components/Deal/AdminController/ADealController.php b/inc/Hura8/Components/Deal/AdminController/ADealController.php new file mode 100644 index 0000000..4ed9df7 --- /dev/null +++ b/inc/Hura8/Components/Deal/AdminController/ADealController.php @@ -0,0 +1,58 @@ +objDealModel->getAllAutoRenewableDeal(); + + //then renew & log history + foreach ($deal_list as $info) { + $this->renewDeal($info['id'], $info['from_time'], $info['to_time'], $info['auto_renew_history']); + } + } + + protected function renewDeal($id, $from_time, $to_time, array $auto_renew_history) { + /*$obj_from_date = new \DateTime(date("Y-m-d", $from_time)); + $obj_to_date = new \DateTime(date("Y-m-d", $to_time)); + $day_diff = $obj_to_date->diff($obj_from_date)->format('%a'); + + $obj_to_date->add(new \DateInterval('P'.$day_diff.'D')); + $new_date = $obj_to_date->format('Y-m-d'); + + $current_date = date("Y-m-d"); + $from_time_minute = ($from_time > 0) ? date("H:i", $from_time) : "00:00"; + $to_time_minute = ($to_time > 0) ? date("H:i", $to_time) : "00:00";*/ + + $to_time_new = CURRENT_TIME + ($to_time - $from_time); + + $auto_renew_history[] = [ + "renew_time" => show_datetime_from_unix(CURRENT_TIME), + "from_time" => show_datetime_from_unix(CURRENT_TIME), + "to_time" => show_datetime_from_unix($to_time_new), + ]; + + return $this->updateFields( $id, + [ + 'from_time' => CURRENT_TIME, + 'to_time' => $to_time_new, + 'auto_renew_history' => serialize($auto_renew_history), + ] + ); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + return true; + } +} diff --git a/inc/Hura8/Components/Deal/Controller/bDealController.php b/inc/Hura8/Components/Deal/Controller/bDealController.php new file mode 100644 index 0000000..8f714d2 --- /dev/null +++ b/inc/Hura8/Components/Deal/Controller/bDealController.php @@ -0,0 +1,48 @@ +objDealModel = new DealModel(); + parent::__construct($this->objDealModel); + } + + + public function getList(array $condition) : array + { + $deal_list = parent::getList($condition); + + $product_list_info = []; + $product_list_ids = []; + $final_list = []; + foreach ($deal_list as $item) { + if(!isset($product_list_info[$item['pro_id']])) { + $product_list_info[$item['pro_id']]= null; + $product_list_ids[] = $item['pro_id']; + } + + $copy = $item; + $copy['product_info'] = &$product_list_info[$item['pro_id']]; + $final_list[] = $copy; + } + + $objAProductController = new AProductController(); + foreach ($objAProductController->getListByIds($product_list_ids) as $pro_id => $info) { + $product_list_info[$pro_id] = $info; + } + + return $final_list; + } + + +} diff --git a/inc/Hura8/Components/Deal/Model/DealCollectionModel.php b/inc/Hura8/Components/Deal/Model/DealCollectionModel.php new file mode 100644 index 0000000..446d3f7 --- /dev/null +++ b/inc/Hura8/Components/Deal/Model/DealCollectionModel.php @@ -0,0 +1,223 @@ +getAllDealIdInCollection($collection_id); + + if(!sizeof($deal_id_list)) return array(); + + $query = $this->db->runQuery("SELECT `pro_id` FROM `".$this->tb_deal."` WHERE `id` IN (". join(',', $deal_id_list).") "); + + return array_map(function ($item){ + return $item['pro_id']; + }, $this->db->fetchAll($query)); + } + + public function getAllDealIdInCollection($collection_id) { + $query = $this->db->runQuery("SELECT `deal_id` FROM `".$this->tb_collection_item."` WHERE `collection_id` = ? ", ['d'], [$collection_id]); + + return array_map(function ($item){ + return $item['deal_id']; + }, $this->db->fetchAll($query)); + } + + public function updateAllDealInCollection($collection_id, $price, $time){ + + $from_time = ($time['from_date'] != '') ? strtotime(TimeManager::convert_date_from_javascript($time['from_date'])." ".$time['from_time_minute'].":00") : 0; + $to_time = ($time['to_date'] != '') ? strtotime(TimeManager::convert_date_from_javascript($time['to_date'])." ".$time['to_time_minute'].":00") : 0; + + $deal_id_list = $this->getAllDealIdInCollection($collection_id); + + if(!sizeof($deal_id_list)) return false; + + $query = $this->db->runQuery("SELECT `id`, `pro_id` FROM `".$this->tb_deal."` WHERE `id` IN (". join(',', $deal_id_list).") "); + $product_list_id = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + $product_list_id[$info['pro_id']] = $info['id']; + } + + //get product prices + $query = $this->db->runQuery(" + SELECT `id`, price FROM ".TableName::PRODUCT." + WHERE `id` IN (". join(',', array_keys($product_list_id)) .") + "); + + $objDealModel = new DealModel(); + + foreach ( $this->db->fetchAll($query) as $_info ) { + $product_id = $_info['id']; + $product_price = $_info['price']; + $deal_price = ($price['type'] == 'percent') ? round($product_price * (100 - $price['value']) / 100) : ($product_price - $price['value']); + + //update + $objDealModel->updatePriceAndTime( + $product_list_id[$product_id], + array( + "price" => $deal_price, + "from_time" => $from_time, + "to_time" => $to_time, + ) + ); + } + + return true; + } + + public function addProductToCollection($product_id, $collection_id){ + + $objAProductController = new AProductController(); + $product_info = $objAProductController->getInfo($product_id); + + if($product_info){ + + $objDealModel = new DealModel(); + + $deal_id = $objDealModel->create(array( + "pro_id" => $product_id, + "title" => $product_info['title'], + "price" => $product_info['price'], + "quantity" => $product_info['quantity'], + "min_purchase" => 1, + "from_time" => 0, + "to_time" => 0, + "ordering" => $product_info[''], + "description" => '', + )); + + $this->addToCollection($deal_id, $collection_id); + + return $deal_id; + } + + return 0; + } + + public function removeFromCollection($deal_id, $collection_id){ + $this->db->runQuery( + "DELETE FROM `".$this->tb_collection_item."` WHERE `deal_id` = ? AND `collection_id` = ? ", + ['d', 'd'], + [$deal_id, $collection_id] + ); + + $this->updateCollectionCount($collection_id); + } + + public function addToCollection($deal_id, $collection_id){ + $query = $this->db->runQuery( + "SELECT `deal_id` FROM `".$this->tb_collection_item."` WHERE `deal_id`= ? AND `collection_id` = ? LIMIT 1 ", + ['d', 'd'], [$deal_id, $collection_id] + ); + + if( ! $this->db->fetchAssoc($query) ){ + $this->db->insert( + $this->tb_collection_item , + [ + 'collection_id' => $collection_id , + 'deal_id' => $deal_id, + 'create_by' => ADMIN_ID, + 'create_time' => CURRENT_TIME, + ] + ); + + $this->updateCollectionCount($collection_id); + } + } + + protected function updateCollectionCount($collection_id){ + $this->db->runQuery( + "UPDATE `".$this->tb_entity."` SET + `deal_count` = (SELECT COUNT(*) FROM `".$this->tb_collection_item."` WHERE `collection_id` = ? ) + WHERE `id` = ? LIMIT 1 ", + ['d', 'd'], + [$collection_id, $collection_id] + ); + } + + public function updateCollectionView($id){ + $this->db->runQuery("UPDATE `".$this->tb_entity."` SET `visit` = `visit` + 1 WHERE `id` = ? LIMIT 1 ", ['d'], [ $id ]); + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + $info = $input_info; + + $info['create_time'] = CURRENT_TIME; + $info['create_by'] = ADMIN_NAME; + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + protected function afterCreateItem($new_item_id, $new_item_info) + { + // TODO: Implement afterCreateItem() method. + } + + protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info) : AppResponse + { + $info = $new_input_info; + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) + { + // TODO: Implement afterUpdateItem() method. + } + + protected function beforeDeleteItem($item_id, $item_info) : AppResponse + { + return new AppResponse('ok'); + } + + protected function afterDeleteItem($item_id, $item_info) + { + // TODO: Implement afterDeleteItem() method. + } + + +} diff --git a/inc/Hura8/Components/Deal/Model/DealModel.php b/inc/Hura8/Components/Deal/Model/DealModel.php new file mode 100644 index 0000000..9bdb007 --- /dev/null +++ b/inc/Hura8/Components/Deal/Model/DealModel.php @@ -0,0 +1,196 @@ +db->runQuery( + "SELECT `id`, `from_time`, `to_time` FROM `". $this->tb_entity ."` + WHERE `to_time` < '".CURRENT_TIME."' AND `to_time` > 0 AND `auto_renew` = 1 " + ); + + return $this->db->fetchAll($query); + } + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + + $where_clause = []; + $bind_types = []; + $bind_values = []; + + $limit_by_time = false; + $limit_by_time_condition = ''; + + // get deal by start time + // require format: YY-mm-dd h:m:i or YY-mm-dd h:m or timestamp + $datetime_pattern = '/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}\s[0-9]{1,2}:[0-9]{1,2}(:[0-9]{1,2})?$/i'; + + if(isset($filter_condition['start_time']) && $filter_condition['start_time'] ) { + $limit_by_time = true; + if(preg_match($datetime_pattern, trim($filter_condition['start_time']))) { + $check_time = strtotime($filter_condition['start_time']); + }else{ + $check_time = intval($filter_condition['start_time']); + } + + $limit_by_time_condition .= " AND `from_time` >= '".$check_time."' "; + + $bind_types[] = 'd'; + $bind_values[] = $check_time; + } + + // get deal by end time + // require format: YY-mm-dd h:m:i or YY-mm-dd h:m or timestamp + if(isset($filter_condition['end_time']) && $filter_condition['end_time'] ) { + $limit_by_time = true; + if(preg_match($datetime_pattern, trim($filter_condition['end_time']))) { + $check_time = strtotime($filter_condition['end_time']); + }else{ + $check_time = intval($filter_condition['end_time']); + } + + $limit_by_time_condition .= " AND `to_time` <= '".$check_time."' "; + $bind_types[] = 'd'; + $bind_values[] = $check_time; + } + + if($limit_by_time) { + $where_clause[] = " AND `status`= 1 ".$limit_by_time_condition; + } + + //type expire + if(isset($filter_condition['type']) && $filter_condition['type'] == 'expire' ) { + $where_clause[] = "AND `status`= 1 AND `to_time` < ? "; + $bind_types[] = 'd'; + $bind_values[] = CURRENT_TIME; + } + + //type active: might not have begun yet + if(isset($filter_condition['type']) && $filter_condition['type'] == 'active' && !$limit_by_time ) { + $where_clause[] = " AND `status` = 1 AND `to_time` >= ? "; + $bind_types[] = 'd'; + $bind_values[] = CURRENT_TIME; + } + + // Đang bắt đầu + if(isset($filter_condition['type']) && $filter_condition['type'] == 'started' && !$limit_by_time ) { + $where_clause[] = " AND `status` = 1 AND `to_time` >= ? AND from_time < ? "; + $bind_types[] = 'd'; + $bind_values[] = CURRENT_TIME; + $bind_types[] = 'd'; + $bind_values[] = CURRENT_TIME; + } + + // Chưa bắt đầu + if(isset($filter_condition['type']) && $filter_condition['type'] == 'coming' && !$limit_by_time ) { + $where_clause[] = " AND `status` = 1 AND `from_time` >= ? "; + $bind_types[] = 'd'; + $bind_values[] = CURRENT_TIME; + } + + //deal collection + if(isset($filter_condition['collection_id']) && $filter_condition['collection_id'] > 0) { + $where_clause[] = " AND `id` IN ( SELECT `deal_id` FROM `".$this->tb_collection_item."` WHERE `collection_id` = ? ) "; + $bind_types[] = 'd'; + $bind_values[] = $filter_condition['collection_id']; + } + + // exclude from collection + if(isset($filter_condition['add_to_collection']) && $filter_condition['add_to_collection'] > 0) { + $where_clause[] = " AND `id` NOT IN ( SELECT `deal_id` FROM `".$this->tb_collection_item."` WHERE `collection_id` = ? ) "; + $bind_types[] = 'd'; + $bind_values[] = $filter_condition['add_to_collection']; + } + + // by word filter + $filter = $filter_condition['filter'] ?? ''; + switch ($filter) { + case "not-started": // Chưa bắt đầu + $where_clause[] = " AND ( ? - `from_time` ) < 0 "; + $bind_types[] = 'd'; + $bind_values[] = CURRENT_TIME; + break; + + case "started": // Đang bắt đầu + $where_clause[] = " AND ( ? - `from_time` ) > 0 AND ( `to_time` - ?) > 0 "; + $bind_types[] = 'd'; + $bind_types[] = 'd'; + $bind_values[] = CURRENT_TIME; + $bind_values[] = CURRENT_TIME; + break; + + case "ended": // Hết thời gian + $where_clause[] = " AND (`to_time` - ?) < 0 "; + $bind_types[] = 'd'; + $bind_values[] = CURRENT_TIME; + break; + + case "hidden": // Ẩn hiển thị + $where_clause[] = " AND `status` = 0 "; + break; + + case "featured": // Đang nổi bật + $where_clause[] = " AND `is_featured` = 1 "; + break; + } + + + return [ + join(" ", $where_clause), + $bind_types, + $bind_values + ]; + } + + + protected function formatItemInList(array $item_info): array + { + $copy = $item_info; + + $copy['deal_time_happen'] = CURRENT_TIME - $item_info['from_time']; + $copy['deal_time_left'] = $item_info['to_time'] - CURRENT_TIME; + $copy['is_start'] = (CURRENT_TIME - $item_info['from_time'] > 0) ? 1 : 0; + $copy['is_end'] = ($item_info['to_time'] - CURRENT_TIME > 0) ? 0 : 1; + + return $copy; + } + + protected function formatItemInfo(array $item_info): array + { + $copy = $item_info; + + $copy['deal_time_happen'] = CURRENT_TIME - $item_info['from_time']; + $copy['deal_time_left'] = $item_info['to_time'] - CURRENT_TIME; + $copy['is_start'] = (CURRENT_TIME - $item_info['from_time'] > 0) ? 1 : 0; + $copy['is_end'] = ($item_info['to_time'] - CURRENT_TIME > 0) ? 0 : 1; + + return $copy; + } + + + +} diff --git a/inc/Hura8/Components/Marketing/AdminController/ACouponController.php b/inc/Hura8/Components/Marketing/AdminController/ACouponController.php new file mode 100644 index 0000000..9b569d0 --- /dev/null +++ b/inc/Hura8/Components/Marketing/AdminController/ACouponController.php @@ -0,0 +1,40 @@ +objCouponModel->updateProduct($product_id, $coupon_id, $info); + } + + + public function removeProduct($product_id, $coupon_id) + { + return $this->objCouponModel->removeProduct($product_id, $coupon_id); + } + + + public function addProduct($product_id, $coupon_id, $ordering=0) + { + return $this->objCouponModel->addProduct($product_id, $coupon_id, $ordering); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // TODO: Implement deleteFileBeforeDeleteItem() method. + return true; + } + + +} diff --git a/inc/Hura8/Components/Marketing/AdminController/APosterController.php b/inc/Hura8/Components/Marketing/AdminController/APosterController.php new file mode 100644 index 0000000..bad0fd4 --- /dev/null +++ b/inc/Hura8/Components/Marketing/AdminController/APosterController.php @@ -0,0 +1,26 @@ +objPosterModel = new PosterModel(); + parent::__construct($this->objPosterModel); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // TODO: Implement deleteFileBeforeDeleteItem() method. + return true; + } +} diff --git a/inc/Hura8/Components/Marketing/AdminController/AProductFeedController.php b/inc/Hura8/Components/Marketing/AdminController/AProductFeedController.php new file mode 100644 index 0000000..e390b22 --- /dev/null +++ b/inc/Hura8/Components/Marketing/AdminController/AProductFeedController.php @@ -0,0 +1,51 @@ +objProductFeedModel->getAllCategories(); + } + + public function getAllProductListIds( $list_id){ + return $this->objProductFeedModel->getAllProductListIds($list_id); + } + + public function getProductListTotal($list_id) { + return $this->objProductFeedModel->getProductListTotal($list_id); + } + + public function getProductList($list_id, $page = 1, $numPerPage = 30) { + return $this->objProductFeedModel->getProductList($list_id, $page, $numPerPage); + } + + public function deleteAllProductFromList($list_id) { + $this->objProductFeedModel->deleteAllProductFromList($list_id); + } + + //remove product from a list + public function deleteProductFromList($pro_list, $list_id){ + return $this->objProductFeedModel->deleteProductFromList($pro_list, $list_id); + } + + //add product to a list + public function addProductToList($pro_list, $list_id){ + return $this->objProductFeedModel->addProductToList($pro_list, $list_id); + } + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + return true; + } +} diff --git a/inc/Hura8/Components/Marketing/Controller/bCouponController.php b/inc/Hura8/Components/Marketing/Controller/bCouponController.php new file mode 100644 index 0000000..96c2fda --- /dev/null +++ b/inc/Hura8/Components/Marketing/Controller/bCouponController.php @@ -0,0 +1,46 @@ + "Tặng sản phẩm", + 'cash' => "Tặng tiền mặt", + 'priceoff' => "Giảm giá %", + 'other' => "Khác" + ); + + + /* @var CouponModel $objCouponModel */ + protected $objCouponModel; + + public function __construct() + { + $this->objCouponModel = new CouponModel(); + parent::__construct($this->objCouponModel); + } + + + public function getTotalProduct($coupon_id, array $condition = []) + { + return $this->objCouponModel->getTotalProduct($coupon_id, $condition); + } + + + public function getListProduct($coupon_id, array $condition = []) { + return $this->objCouponModel->getListProduct($coupon_id, $condition); + } + + + public function getTypeList() { + return $this->type_list; + } + + +} diff --git a/inc/Hura8/Components/Marketing/Controller/bProductFeedController.php b/inc/Hura8/Components/Marketing/Controller/bProductFeedController.php new file mode 100644 index 0000000..7dfdb6d --- /dev/null +++ b/inc/Hura8/Components/Marketing/Controller/bProductFeedController.php @@ -0,0 +1,26 @@ +objProductFeedModel = new ProductFeedModel(); + parent::__construct($this->objProductFeedModel); + } + + + public function getInfoByPublicId($public_id) + { + return $this->objProductFeedModel->getInfoByPublicId($public_id); + } + + +} diff --git a/inc/Hura8/Components/Marketing/Model/CouponModel.php b/inc/Hura8/Components/Marketing/Model/CouponModel.php new file mode 100644 index 0000000..8d1896a --- /dev/null +++ b/inc/Hura8/Components/Marketing/Model/CouponModel.php @@ -0,0 +1,130 @@ +db->runQuery( + " SELECT COUNT(*) as total FROM `".$this->tb_coupon_product."` WHERE `coupon_id` = ? ", + ['d'], [$coupon_id] + ); + + $total = 0; + if ($rs = $this->db->fetchAssoc($query)) { + $total = $rs['total']; + } + + return $total; + } + + + public function getListProduct($coupon_id, array $condition = []) + { + $numPerPage = (isset($condition['numPerPage']) && $condition['numPerPage'] > 0 ) ? intval($condition['numPerPage']) : 20 ; + $page = (isset($condition['page']) && $condition['page'] > 0 ) ? intval($condition['page']) : 1 ; + $order_by = " `ordering` DESC, `id` DESC"; + + $query = $this->db->runQuery( + "SELECT `product_id` FROM ".$this->tb_coupon_product." WHERE `coupon_id` = ? + ORDER BY ".$order_by." + LIMIT ".(($page-1) * $numPerPage).", ".$numPerPage , + ['d'], [$coupon_id] + ) ; + + $item_list = array(); + $counter = ($page-1) * $numPerPage; + foreach ( $this->db->fetchAll($query) as $item ) { + $counter += 1; + + $item_list[$item['product_id']] = [ + 'counter' => $counter, + ]; + } + + $objProductModel = new ProductModel(); + $product_list_info = $objProductModel->getListByIds(array_keys($item_list)); + + // final list + $final_list = []; + foreach ($item_list as $_pro_id => $_pro_info_in_collection) { + $pro_basic = $product_list_info[$_pro_id] ?? null; + if($pro_basic) { + $final_list[] = array_merge($pro_basic, $_pro_info_in_collection); + } + } + + return $final_list; + } + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + /*$condition = array( + "q" => getRequest("q", ''), + "featured" => (int) getRequest("featured"), + "status" => (int) getRequest("status"), + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + if(isset($filter_condition["letter"]) && strlen($filter_condition["letter"]) == 1){ + $catCondition[] = " AND `letter` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["letter"]; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function createUniqueCode($current_item_id, $wanted_code = ''){ + if(!$wanted_code) $wanted_code = IDGenerator::createStringId(10); + + $clean_code = DataClean::makeInputSafe($wanted_code, DataType::ID); + $clean_code = strtoupper($clean_code); + + //if exist and belong other id, create a new one + $query = $this->db->runQuery("SELECT `id` FROM `".$this->tb_entity."` WHERE `code` = ? LIMIT 1 ", ['s'], [$clean_code]) ; + if($info = $this->db->fetchAssoc($query)){ + if($info['id'] != $current_item_id) { + $new_code = IDGenerator::createStringId(6); + return $this->createUniqueCode($current_item_id, $new_code); + } + } + + return $clean_code; + } + + +} diff --git a/inc/Hura8/Components/Marketing/Model/PosterModel.php b/inc/Hura8/Components/Marketing/Model/PosterModel.php new file mode 100644 index 0000000..151133b --- /dev/null +++ b/inc/Hura8/Components/Marketing/Model/PosterModel.php @@ -0,0 +1,33 @@ +all_brands = $this->buildAllBrands(); + //$this->all_categories = $this->buildAllCategories(); + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function getInfoByPublicId($public_id) + { + $query = $this->db->runQuery("SELECT * FROM `".$this->tb_entity."` WHERE `public_id` = ? LIMIT 1 ", ['s'], [$public_id]) ; + if( $item_info = $this->db->fetchAssoc($query)){ + return $this->formatItemInfo($item_info); + } + + return false; + } + + public function getProductListTotal($list_id) { + $query = $this->db->runQuery( + " SELECT COUNT(*) AS total FROM `".$this->tb_feed_product."` WHERE `list_id` = ? ", + ['d'], [$list_id] + ); + + $total = 0; + if ($info = $this->db->fetchAssoc($query)) { + $total = $info['total']; + } + + return $total; + } + + public function getProductList($list_id, $page = 1, $numPerPage = 30) { + + $query = $this->db->runQuery( + " SELECT `pro_id` FROM `".$this->tb_feed_product."` + WHERE `list_id` = ? + ORDER BY `id` DESC + LIMIT ".( ($page - 1) * $numPerPage ).", ".$numPerPage, + ['d'], [$list_id] + ); + + $product_list_ids = array_map(function ($item) { return $item['pro_id'];}, $this->db->fetchAll($query)); + + $objAProductController = new AProductController(); + + $stt = ($page - 1) * $numPerPage; + $item_list = []; + foreach ($objAProductController->getListByIds($product_list_ids) as $_id => $info) { + $stt++; + $info["counter"] = $stt; + $item_list[] = $info; + } + + return $item_list; + } + + //-------------------- + + public function getAllCategories() { + return $this->all_categories; + } + + + public function getAllProductListIds( $list_id){ + if(!$list_id) return []; + + $query = $this->db->runQuery("SELECT `pro_id` FROM `".$this->tb_feed_product."` WHERE `list_id` = ? ", ['d'], [ $list_id ]); + + $item_list = array(); + foreach ( $this->db->fetchAll($query) as $rs ) { + $item_list[] = $rs['pro_id']; + } + + return $item_list; + } + + + //-------------------- + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + /*$condition = array( + "letter" => "", + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + if(isset($filter_condition["letter"]) && strlen($filter_condition["letter"]) == 1){ + $catCondition[] = " AND `letter` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["letter"]; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + +} diff --git a/inc/Hura8/Components/Marketing/Model/UProductFeedModel.php b/inc/Hura8/Components/Marketing/Model/UProductFeedModel.php new file mode 100644 index 0000000..0883921 --- /dev/null +++ b/inc/Hura8/Components/Marketing/Model/UProductFeedModel.php @@ -0,0 +1,8 @@ +getInfo($item_id); + if(!$item_info['file_url']) { + return true; + } + + $file_name = $item_info['file_url']; + + foreach (static::$resized_sizes as $size => $value) { + $file_local_path = PUBLIC_DIR . "/". static::$image_folder . "/". $size. IMAGE_FILE_SEPARATOR . $file_name; + @unlink($file_local_path); + } + + // remove original file + $file_local_path = PUBLIC_DIR . "/". static::$image_folder . "/". $file_name; + @unlink($file_local_path); + + return true; + } + +} diff --git a/inc/Hura8/Components/Media/AdminController/AMediaCategoryController.php b/inc/Hura8/Components/Media/AdminController/AMediaCategoryController.php new file mode 100644 index 0000000..6fd0203 --- /dev/null +++ b/inc/Hura8/Components/Media/AdminController/AMediaCategoryController.php @@ -0,0 +1,14 @@ +getInfo($item_id); + if(!$item_info['file_url']) { + return true; + } + + list($sub_folder, $file_name) = explode("/", $item_info['file_url']); // format: Y-m-d/file_name + + foreach (static::$resized_sizes as $size => $value) { + $file_local_path = PUBLIC_DIR . "/". static::$image_folder . "/" . $sub_folder . "/". $size. IMAGE_FILE_SEPARATOR . $file_name; + @unlink($file_local_path); + } + + // remove original file + $file_local_path = PUBLIC_DIR . "/". static::$image_folder . "/" . $sub_folder . "/". $file_name; + @unlink($file_local_path); + + return true; + } + +} diff --git a/inc/Hura8/Components/Media/Controller/bItemMediaController.php b/inc/Hura8/Components/Media/Controller/bItemMediaController.php new file mode 100644 index 0000000..d370ea3 --- /dev/null +++ b/inc/Hura8/Components/Media/Controller/bItemMediaController.php @@ -0,0 +1,61 @@ + ['width' => 300,] + ); + + /* @var ItemMediaModel $objItemMediaModel */ + protected $objItemMediaModel; + + protected $item_type; + protected $item_id; + + public function __construct(string $item_type = '', $item_id = 0) + { + $this->objItemMediaModel = new ItemMediaModel($item_type, $item_id); + parent::__construct($this->objItemMediaModel); + } + + + protected function formatItemInList(array $item_info) : array + { + return $this->formatItemInfo($item_info); + } + + + protected function formatItemInfo(array $item_info) : ?array + { + if(!$item_info) return null; + + $item_info['display_file'] = STATIC_DOMAIN . "/". static::$image_folder. "/". $item_info['file_url']; + $item_info['image'] = self::getResizedImageCollection($item_info['file_url']); + + return $item_info; + } + + + public static function getResizedImageCollection($image_name) { + $image = []; + + $size_in_full = [ + 's' => 'small' , + ]; + + foreach (static::$resized_sizes as $size => $value) { + $image[$size_in_full[$size]] = ($image_name) ? STATIC_DOMAIN . "/". static::$image_folder . "/". $size. IMAGE_FILE_SEPARATOR . $image_name : ''; + } + + return $image; + } + +} diff --git a/inc/Hura8/Components/Media/Controller/bMediaCategoryController.php b/inc/Hura8/Components/Media/Controller/bMediaCategoryController.php new file mode 100644 index 0000000..6ac394e --- /dev/null +++ b/inc/Hura8/Components/Media/Controller/bMediaCategoryController.php @@ -0,0 +1,20 @@ +objMediaCategoryModel = new MediaCategoryModel(); + parent::__construct($this->objMediaCategoryModel); + } + +} diff --git a/inc/Hura8/Components/Media/Controller/bMediaController.php b/inc/Hura8/Components/Media/Controller/bMediaController.php new file mode 100644 index 0000000..637ab9e --- /dev/null +++ b/inc/Hura8/Components/Media/Controller/bMediaController.php @@ -0,0 +1,59 @@ + ['width' => 300,] + ); + + protected $objMediaModel; + + + public function __construct() + { + $this->objMediaModel = new MediaModel(); + parent::__construct($this->objMediaModel); + } + + + protected function formatItemInList(array $item_info) : array + { + return $this->formatItemInfo($item_info); + } + + + protected function formatItemInfo(array $item_info) : ?array + { + if(!$item_info) return null; + + $item_info['display_file'] = STATIC_DOMAIN . "/". static::$image_folder. "/". $item_info['file_url']; + $item_info['image'] = ($item_info['file_type'] == 'image') ? self::getResizedImageCollection($item_info['file_url']) : null; + + return $item_info; + } + + + public static function getResizedImageCollection($image_name) { + $image = []; + + $size_in_full = [ + 's' => 'small' , + ]; + + foreach (static::$resized_sizes as $size => $value) { + $image[$size_in_full[$size]] = ($image_name) ? STATIC_DOMAIN . "/". static::$image_folder . "/". $size. IMAGE_FILE_SEPARATOR . $image_name : ''; + } + + return $image; + } + +} diff --git a/inc/Hura8/Components/Media/Model/ItemMediaModel.php b/inc/Hura8/Components/Media/Model/ItemMediaModel.php new file mode 100644 index 0000000..57d2a59 --- /dev/null +++ b/inc/Hura8/Components/Media/Model/ItemMediaModel.php @@ -0,0 +1,76 @@ +item_type = $item_type; + $this->item_id = $item_id; + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + if($this->item_type) { + $catCondition[] = " AND `item_type` = ? "; + $bind_types[] = 's'; + $bind_values[] = $this->item_type; + } + + if($this->item_id) { + $catCondition[] = " AND `item_id` = ? "; + $bind_types[] = 'd'; + $bind_values[] = $this->item_id; + } + + if(isset($filter_condition["item_type"]) && $filter_condition["item_type"]){ + $catCondition[] = " AND `item_type` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["item_type"]; + } + + if(isset($filter_condition["item_id"]) && $filter_condition["item_id"]){ + $catCondition[] = " AND `item_id` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["item_id"]; + } + + if(isset($filter_condition["file_type"]) && $filter_condition["file_type"]){ + $catCondition[] = " AND `file_type` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["file_type"]; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + + +} diff --git a/inc/Hura8/Components/Media/Model/ItemMediaSearchModel.php b/inc/Hura8/Components/Media/Model/ItemMediaSearchModel.php new file mode 100644 index 0000000..f92f5e2 --- /dev/null +++ b/inc/Hura8/Components/Media/Model/ItemMediaSearchModel.php @@ -0,0 +1,34 @@ + "tb_item_media.item_type", + 'item_id' => "tb_item_media.item_id", + 'file_type' => "tb_item_media.file_type", + ]; + + private $fulltext_fields = [ + "keywords" => ["tb_item_media.title",], + ]; + + + public function __construct() + { + parent::__construct( + "tb_item_media", + $this->fulltext_fields, + $this->filter_fields + ); + + //$this->createTableSearch(); + } + +} diff --git a/inc/Hura8/Components/Media/Model/MediaCategoryModel.php b/inc/Hura8/Components/Media/Model/MediaCategoryModel.php new file mode 100644 index 0000000..86cd5cd --- /dev/null +++ b/inc/Hura8/Components/Media/Model/MediaCategoryModel.php @@ -0,0 +1,20 @@ + getRequestInt("category"), + "file_type" => '', + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + //Tim danh muc + if(isset($filter_condition["category"]) && $filter_condition["category"]){ + $catCondition[] = " AND `id` IN (SELECT `item_id` FROM `".$this->tb_media_per_category."` WHERE `category_id` = ?) "; + $bind_types[] = 'd'; + $bind_values[] = $filter_condition["category"]; + } + + if(isset($filter_condition["file_type"]) && $filter_condition["file_type"]){ + $catCondition[] = " AND `file_type` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["file_type"]; + } + + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + +} diff --git a/inc/Hura8/Components/Media/Model/MediaSearchModel.php b/inc/Hura8/Components/Media/Model/MediaSearchModel.php new file mode 100644 index 0000000..45c3890 --- /dev/null +++ b/inc/Hura8/Components/Media/Model/MediaSearchModel.php @@ -0,0 +1,32 @@ + "tb_media.file_type", + ]; + + private $fulltext_fields = [ + "keywords" => ["tb_media.title",], + ]; + + + public function __construct() + { + parent::__construct( + "tb_media", + $this->fulltext_fields, + $this->filter_fields + ); + + //$this->createTableSearch(); + } + +} diff --git a/inc/Hura8/Components/Order/AdminController/AOrderController.php b/inc/Hura8/Components/Order/AdminController/AOrderController.php new file mode 100644 index 0000000..1e3b86c --- /dev/null +++ b/inc/Hura8/Components/Order/AdminController/AOrderController.php @@ -0,0 +1,235 @@ +objAOrderStatusController = new AOrderStatusController(); + $this->objOrderModel = new OrderModel(); + parent::__construct($this->objOrderModel); + } + + public function updateOrderFulfillmentStatus($order_id, $new_status, $comment, $payment_data = []) { + + if(!array_key_exists($new_status, OrderStatus::FULFILLMENT_STATUS)) { + return [ + "status" => 'error', + 'message' => '', + ]; + } + + $this->objAOrderStatusController->createHistory($order_id, 'fulfillment', $new_status, $comment, $payment_data ); + + //cap nhat don hang + $this->objOrderModel->update( $order_id, [ + 'fulfillment_status' => $new_status + ]); + + $this->updateDerivedOrderStatus($order_id); + + return [ + "status" => 'success', + 'message' => 'Cập nhật thành công', + ]; + } + + public function updateOrderPaymentStatus($order_id, $new_status, $comment, $payment_data = []) { + + if(!array_key_exists($new_status, OrderStatus::PAYMENT_STATUS)) { + return [ + "status" => 'error', + 'message' => '', + ]; + } + + $this->objAOrderStatusController->createHistory($order_id, 'payment', $new_status, $comment, $payment_data ); + + //cap nhat don hang + $this->objOrderModel->update( $order_id, [ + 'payment_status' => $new_status + ]); + + $this->updateDerivedOrderStatus($order_id); + + return [ + "status" => 'success', + 'message' => 'Cập nhật thành công', + ]; + } + + protected function updateDerivedOrderStatus($order_id) { + + $current_info = $this->getInfo($order_id); + + // order must be success + if( + $current_info['payment_status'] == OrderStatus::PAYMENT_STATUS['paid']['id'] && + $current_info['fulfillment_status'] == OrderStatus::FULFILLMENT_STATUS['fulfilled']['id'] + ) { + + return $this->objOrderModel->update($order_id, [ + 'order_status' => OrderStatus::ORDER_STATUS['success']['id'] + ]); + + } + + + // update processing + return $this->objOrderModel->update($order_id, [ + 'order_status' => OrderStatus::ORDER_STATUS['processing']['id'] + ]); + + } + + public function updateOrderStatus($order_id, $status, $comment) { + + $current_info = $this->getInfo($order_id); + + // cannot update if order has: success or cancel + if(in_array($current_info['order_status'], [ + OrderStatus::ORDER_STATUS['success']['id'], + OrderStatus::ORDER_STATUS['canceled']['id'], + ])) { + return [ + "status" => 'error', + 'message' => '', + ]; + } + + if(!array_key_exists($status, OrderStatus::ORDER_STATUS)) { + return [ + "status" => 'error', + 'message' => '', + ]; + } + + $this->objAOrderStatusController->createHistory($order_id, 'order', $status, $comment ); + + $this->objOrderModel->update($order_id, [ + 'order_status' => $status + ]); + + // dispatch + $objOrderEvent = new OrderEvent(); + $objOrderEvent->setUpdateOrderInfo([ + 'orderId' => $order_id, + 'order_status' => $status, + 'old_status' => $current_info['order_status'] + ]); + get_dispatcher()->dispatch(EventName::ORDER['updated_status'], $objOrderEvent); + + return [ + "status" => 'success', + 'message' => 'Cập nhật thành công', + ]; + } + + + protected function formatItemInList(array $order_info ) { + + $rs = $order_info; + + $rs["order_date"] = date("d-m-Y", $rs['order_time']); + $rs["order_hour"] = date("h:i a", $rs['order_time']); + + if($rs['order_discount']) $rs['order_discount'] = \json_decode($rs['order_discount'], true); + + $rs["payment_status_name"] = (array_key_exists($rs["payment_status"], OrderStatus::PAYMENT_STATUS)) ? OrderStatus::PAYMENT_STATUS[$rs["payment_status"]]['title'] : ''; + + $rs["fulfillment_status_name"] = (array_key_exists($rs["fulfillment_status"], OrderStatus::FULFILLMENT_STATUS)) ? OrderStatus::FULFILLMENT_STATUS[$rs["fulfillment_status"]]['title'] : ''; + + $rs["order_status_name"] = (array_key_exists($rs["order_status"], OrderStatus::ORDER_STATUS)) ? OrderStatus::ORDER_STATUS[$rs["order_status"]]['title'] : ''; + + return $rs; + } + + protected function getListFilterCondition($list_id) { + + if($list_id == 'mine') { + return [ + 'assign_to' => ADMIN_ID, + ]; + } + + if($list_id == 'new') { + return [ + 'status' => 'new', + ]; + } + + if($list_id == 'unpaid') { + return [ + 'payment' => 'unpaid', + ]; + } + + if($list_id == 'partially-paid') { + return [ + 'payment' => 'partially-paid', + ]; + } + + if($list_id == 'unfulfilled') { + return [ + 'fullfillment' => 'unfulfilled', + ]; + } + + if($list_id == 'partially-fulfilled') { + return [ + 'fullfillment' => 'partially-fulfilled', + ]; + } + + return null; + } + + public function getStatusHistory($orderId){ + + return $this->objAOrderStatusController->getStatusHistory($orderId); + + } + + public function getItemsForOrderList(array $list_ids, $fields = "*") { + + return $this->objOrderModel->getItemsForOrderList($list_ids, $fields); + + } + + public static function createId($order_id){ + $key_length = strlen($order_id); + + $order_keys = []; + for($i=0; $i < 9 - $key_length; $i++){ + $order_keys[] = "0"; + } + $order_keys[] = $order_id; + + $order_eles = str_split(join("", $order_keys), 3); + + return join("-", $order_eles); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + return true; + } + +} diff --git a/inc/Hura8/Components/Order/AdminController/AOrderStatusController.php b/inc/Hura8/Components/Order/AdminController/AOrderStatusController.php new file mode 100644 index 0000000..a4773f4 --- /dev/null +++ b/inc/Hura8/Components/Order/AdminController/AOrderStatusController.php @@ -0,0 +1,43 @@ +objOrderStatusModel = new OrderStatusModel(); + } + + public function getStatusHistory($orderId){ + return $this->objOrderStatusModel->getStatusHistory($orderId); + } + + public function createHistory($order_id, $status_type = 'order', $system_status = '', $comment = '', array $data= [], array $other_info = []) { + return $this->objOrderStatusModel->createHistory($order_id, $status_type , $system_status , $comment, $data, $other_info); + } + + public function getInfo($status_id) { + return $this->objOrderStatusModel->getInfo($status_id); + } + + public function getAll() { + return $this->objOrderStatusModel->getAll(); + } + + public function delete($id) { + return $this->objOrderStatusModel->delete($id); + } + + public function update($id, array $info) { + return $this->objOrderStatusModel->update($id, $info); + } + + public function create(array $info) { + return $this->objOrderStatusModel->create($info); + } +} diff --git a/inc/Hura8/Components/Order/Controller/OrderStatus.php b/inc/Hura8/Components/Order/Controller/OrderStatus.php new file mode 100644 index 0000000..69aefe3 --- /dev/null +++ b/inc/Hura8/Components/Order/Controller/OrderStatus.php @@ -0,0 +1,43 @@ + ['id' => 'new', 'title' => 'Mới'],// 'Mới', // + 'processing' => ['id' => 'processing', 'title' => 'Đang xử lý'],// 'Đang xử lý', // + 'success' => ['id' => 'success', 'title' => 'Thành công'],// 'Thành công', // + 'closed' => ['id' => 'closed', 'title' => 'Đóng lại'],// 'Đóng lại', // + //'archived' => 'Lưu kho', // + 'canceled' => ['id' => 'canceled', 'title' => 'Hủy'],// 'Hủy', // + ]; + + const PAYMENT_STATUS = [ + 'pending' => ['id' => 'pending', 'title' => 'Chờ'],// 'Chờ', // Orders have the Payment pending status for one of the following reasons: During checkout, You create an order, Manual payment methods, such as Cash On Delivery + 'authorized' => ['id' => 'authorized', 'title' => 'Đã xác nhận'],// 'Đã xác nhận', // The payment provider validated your customer’s payment information. The order is created, customers can complete their checkout, and inventory is reserved. + 'overdue' => ['id' => 'overdue', 'title' => 'Quá hạn'],// 'Quá hạn', // Payment wasn't captured before the due date that was set in the payment terms on an order that had the Payment pending status. + 'expiring' => ['id' => 'expiring', 'title' => 'Sắp hết hạn'],// 'Sắp hết hạn', // Expiring isn't a payment status, but the Expiring badge is displayed two days before the deadline for capturing payment on orders that have the Authorized payment status. + 'expired' => ['id' => 'expired', 'title' => 'Hết hạn'],// 'Hết hạn', // Payment wasn't captured before the date that was set by the payment provider on an order that had the Authorized payment status. + 'paid' => ['id' => 'paid', 'title' => 'Đã thanh toán'],// 'Đã thanh toán', // Payment was automatically or manually captured, or the order was marked as paid. + 'refunded' => ['id' => 'refunded', 'title' => 'Đã hoàn lại'],// 'Đã hoàn lại', // The full amount that the customer paid for an order was returned to the customer. + 'partially-refunded' => ['id' => 'partially-refunded', 'title' => 'Hoàn lại 1 phần'],// 'Hoàn lại 1 phần', // The amount that was returned to a customer is less than the full amount that the customer paid for an order. + 'partially-paid' => ['id' => 'partially-paid', 'title' => 'Thanh toán 1 phần'],// 'Thanh toán 1 phần', // You manually captured payment for the order and specified less than the full order value as the payment amount. + 'voided' => ['id' => 'voided', 'title' => 'Hủy'],// 'Hủy', // An unpaid order was manually canceled. + 'unpaid' => ['id' => 'unpaid', 'title' => 'Chưa thanh toán'],// 'Chưa thanh toán', // Unpaid payment status includes orders that are in Authorized, Pending, Expired, and Partially paid payment status. + ]; + + const FULFILLMENT_STATUS = [ + 'fulfilled' => ['id' => 'fulfilled', 'title' => 'Đã chuyển hết'],// 'Đã chuyển hết', // When you've shipped all the items in an order, it is Fulfilled. + 'unfulfilled' => ['id' => 'unfulfilled', 'title' => 'Chưa chuyển'],// 'Chưa chuyển', // When an order is placed, it has an Unfulfilled fulfillment status, unless you have selected to automatically capture the payment and automatically fulfill all of the order's line items in the checkout settings. + 'partially-fulfilled' => ['id' => 'partially-fulfilled', 'title' => 'Chuyển 1 phần'],// 'Chuyển 1 phần', // If you have shipped some, but not all, of the items in an order, then the order has a Partially fulfilled fulfillment status. + 'scheduled' => ['id' => 'scheduled', 'title' => 'Đã lên kế hoạch'],// 'Đã lên kế hoạch', // Prepaid subscription orders have a Scheduled status until the fulfillment date is reached + 'onhold ' => ['id' => 'onhold', 'title' => 'Giữ hàng'],// 'Giữ hàng', // When upsell offers are presented to customers at checkout, the order fulfillment status is set to On hold temporarily. When a fulfillment is On hold you can reserve inventory for the order, but you can't fulfill the order until the fulfillment hold is released, and the order fulfillment status changes to Unfulfilled. + ]; + +} + diff --git a/inc/Hura8/Components/Order/Model/OrderModel.php b/inc/Hura8/Components/Order/Model/OrderModel.php new file mode 100644 index 0000000..0803480 --- /dev/null +++ b/inc/Hura8/Components/Order/Model/OrderModel.php @@ -0,0 +1,557 @@ +objSearchModel = new OrderSearchModel(); + + parent::__construct(EntityType::ORDER, '', $this->objSearchModel); + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function getItems($orderId, $fields = "*") { + $query = $this->db->runQuery( + " SELECT ".$fields." FROM `".$this->tb_order_detail."` WHERE `order_id` = ? ", + ['d'], [ $orderId ]); + + $order_item = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + + $item_info = ($info["item_info"]) ? \json_decode($info["item_info"], true) : false; + $in_cart = ($info["in_cart"]) ? \json_decode($info["in_cart"], true) : false; + + + $info['item_info'] = $item_info; + $info['in_cart'] = $in_cart; + + $order_item[] = $info; + } + + return $order_item; + } + + //since we save the entire cart-structure for item_info, we need to re-construct it to make it easier to read and for various reporting/ email etc.. + protected function buildOrderItemInfo($item_type, array $item_info) { + $new_info = []; + + if($item_type == 'product') { + + $new_info = System::stripProperty(array($item_info['info']), [ + "variant_option", + 'id', + 'productId', + 'priceUnit', + 'price', + 'currency', + 'lastUpdate', + 'warranty', + 'productName', + 'productUrl', + 'productModel', + 'productSummary', + 'marketPrice', + 'productImage', + 'brand', + 'visit', + 'rating', + 'reviewCount', + 'quantity', + 'productSKU', + 'hasVAT', + 'condition', + 'specialOffer', + 'specialOfferGroup', + 'productType', + 'thum_poster', + 'promotion_price', + 'addon' + ])[0]; + + //find variants bought + if($new_info['config_count'] > 0) { + + foreach ($item_info['in_cart'] as $_variant) { + $new_info['variants'][] = array( + "id" => $_variant['id'], + "sku" => $_variant['sku'], + "title" => $_variant['name'], + "image" => ($_variant['image']) ? $_variant['image'] : $item_info['info']['productImage']["medium"], + "url" => $item_info['info']['productUrl'], + "attribute" => $_variant['attribute'], + "price" => $_variant['price'], + "addon_total" => 0, + "currency" => $item_info['info']['currency'], + "priceUnit" => $item_info['info']['priceUnit'], + "quantity" => $_variant['quantity'], + "buyer_note" => $_variant['buyer_note'], + "addon" => [], + "promotion" => [ + "list" => $item_info['info']['specialOffer'], + "group" => (isset($_variant['promotion'])) ? $this->buildPromotionFromGroup( $item_info['info']['specialOfferGroup'], $_variant['promotion']) : $this->buildPromotionFromGroup( $item_info['info']['specialOfferGroup'], array() ), + ], + //..any other + ); + } + + }else{ + + //product does not have variant + $build_addon = []; + $total_addon_price = 0; + if(isset($item_info['info']['addon']) && isset($item_info['in_cart'][0]['addon'])) { + $build_addon = $this->getSelectedAddon($item_info['info']['addon'], $item_info['in_cart'][0]['addon']); + foreach ($build_addon as $addon) { + $total_addon_price += $addon['price'] * $addon['quantity']; + } + } + + $variant_in_cart = (isset($item_info['in_cart'][0])) ? $item_info['in_cart'][0] : false; + + $new_info['variants'][] = array( + "id" => 0, + "sku" => $item_info['info']['productSKU'], + "title" => $item_info['info']['productName'], + "image" => $item_info['info']['productImage']["medium"], + "url" => $item_info['info']['productUrl'], + "attribute" => [], + "price" => $item_info['info']['price'], + "addon_total" => $total_addon_price, + "currency" => $item_info['info']['currency'], + "priceUnit" => $item_info['info']['priceUnit'], + "quantity" => $variant_in_cart['quantity'], + "buyer_note" => $variant_in_cart['buyer_note'], + "addon" => $build_addon, + "promotion" => [ + "list" => $item_info['info']['specialOffer'], + "group" => (isset($variant_in_cart['promotion'])) ? $this->buildPromotionFromGroup( $item_info['info']['specialOfferGroup'], $variant_in_cart['promotion']) : $this->buildPromotionFromGroup( $item_info['info']['specialOfferGroup'], array() ), + ], + //..any other + ); + } + + // + return $new_info; + } + + if($item_type == 'combo') { + + $new_info = [ + "price" => $item_info['info']['sale_price'], + "quantity" => $item_info['in_cart']['quantity'], + "buyer_note" => $item_info['in_cart']['buyer_note'], + "url" => '', + ]; + + $new_info['product_list'] = System::stripProperty($item_info['info']['product_list'], [ + "variant_option", + 'id', + 'productId', + 'priceUnit', + 'price', + 'currency', + 'lastUpdate', + 'warranty', + //'productName', + //'productUrl', + //'productModel', + 'productSummary', + 'marketPrice', + 'productImage', + 'brand', + 'visit', + 'rating', + 'reviewCount', + 'config_count', + 'quantity', + //'productSKU', + 'hasVAT', + 'condition', + 'specialOffer', + 'specialOfferGroup', + 'productType', + 'thum_poster', + 'promotion_price', + 'addon', + 'imageCollection', + 'variants' + ]); + + return $new_info; + } + + if($item_type == 'deal') { + + $new_info = [ + "price" => $item_info['info']['price'], + "quantity" => $item_info['in_cart']['quantity'], + "buyer_note" => $item_info['in_cart']['buyer_note'], + "url" => '/deal/'.$item_info['info']['id'], + ]; + + return $new_info; + } + + return $new_info; + } + + protected function getSelectedAddon($all_product_addons, $in_cart) { + + if(!is_array($all_product_addons) || !is_array($in_cart)) return []; + + $result = []; + foreach ($all_product_addons as $item) { + foreach ($in_cart as $selected) { + if($item['addon_id'] == $selected['id']) { + $result[] = [ + "id" => $selected['id'], + "title" => $item['title'], + "price" => $item['price'], + "quantity" => (isset($selected['quantity'])) ? intval($selected['quantity']) : 1, + "related_article_url" => $item['related_article_url'], + ]; + } + } + } + + return $result; + } + + protected function buildPromotionFromGroup( $offer_group, $selected_one_promotion) { + + if(!is_array($offer_group) && !is_array($selected_one_promotion)) return false; + + $promotion = []; + + foreach ($offer_group as $group) { + if($group['type'] == 'one') { + + $selected_promotion = []; + foreach ($group['promotion'] as $promo) { + foreach ($selected_one_promotion as $selected) { + if($selected['promotion_id'] == $promo['id'] && $selected['group_id'] == $group['id']) { + $selected_promotion[] = $promo; + } + } + } + + $promotion[] = [ + "id" => $group['id'], + "title" => $group['title'], + "note" => $group['note'], + "promotion" => $selected_promotion, + ]; + + + }else{ + $promotion[] = [ + "id" => $group['id'], + "title" => $group['title'], + "note" => $group['note'], + "promotion" => $group['promotion'], + ]; + } + } + + return $promotion; + + } + + + protected function getListFilterCondition($list_id) { + + if($list_id == 'mine') { + return [ + 'assign_to' => ADMIN_ID, + ]; + } + + if($list_id == 'new') { + return [ + 'status' => 'new', + ]; + } + + if($list_id == 'unpaid') { + return [ + 'payment' => 'unpaid', + ]; + } + + if($list_id == 'partially-paid') { + return [ + 'payment' => 'partially-paid', + ]; + } + + if($list_id == 'unfulfilled') { + return [ + 'fullfillment' => 'unfulfilled', + ]; + } + + if($list_id == 'partially-fulfilled') { + return [ + 'fullfillment' => 'partially-fulfilled', + ]; + } + + return null; + } + + public function getStatusHistory($orderId){ + $query = $this->db->runQuery(" + select * from ".TB_ORDER_STATUS_HISTORY." + WHERE order_id = ? + order by id desc + limit 100 + ", ['d'], [ $orderId ] ) ; + + return $this->db->fetchAll($query); + } + + public function getItemsForOrderList(array $list_ids, $fields = "*") { + + if(!sizeof($list_ids)) return []; + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($list_ids, 'int'); + + $query = $this->db->runQuery(" + SELECT ".$fields." FROM `".$this->tb_order_detail."` + WHERE `order_id` IN (".$parameterized_ids.") + ", $bind_types, $list_ids); + + $result = array(); + foreach ( $this->db->fetchAll($query) as $rs ) { + + $item_info = unserialize($rs['item_info']); + if( isset($item_info["info"]) && isset($item_info["in_cart"])) { + $rs['item_info'] = $this->buildOrderItemInfo($rs['item_type'], $item_info ); + } else { + $rs['item_info'] = null; + } + + $result[$rs['order_id']][] = $rs; + } + + return $result; + } + + protected function _buildQueryConditionExtend(array $conditions) : ?array + { + $catCondition = []; + $bind_types = []; + $bind_values = []; + + /* + $conditions = array( + 'orderCode' => '', + 'query' => '', + 'coupon' => '', + 'cus_id' => '', + 'province' => '', + 'district' => '', + 'folder' => '', + 'view_status' => '', + 'update_by' => '', + 'shipping_status' => '', + 'assign_to' => '', + 'from_date' => '', + 'to_date' => '', + 'from_hour' => '', + 'to_hour' => '', + 'excluded_ids' => '', + 'included_ids' => '', + 'payment' => '', + 'fullfillment' => '', + 'list' => '', + );*/ + + + // merge with special list + if(isset($conditions['list']) && $conditions['list']) { + $list_condition = $this->getListFilterCondition($conditions['list']); + if($list_condition) { + // update and over-write any key in $conditions if exist + foreach ($list_condition as $key => $value) { + $conditions[$key] = $value; + } + } + } + + if(isset($conditions['orderCode']) && $conditions['orderCode']) { + $orderCode = DataClean::makeInputSafe($conditions['orderCode'],DataType::INTEGER); + $catCondition[] = " AND `orderId` LIKE '".$orderCode."%' "; + } + + if(isset($conditions['coupon']) && $conditions['coupon']) { + $coupon = preg_replace("/[^a-z0-9_\-\.]/i","", $conditions['coupon']); + if($coupon) $catCondition[] = " AND ( LENGTH(`order_discount`) > 10 AND `order_discount` LIKE '%".$coupon."%' ) "; + } + + if(isset($conditions['cus_id']) && $conditions['cus_id']) { + $catCondition[] = " AND buyerId = '".intval($conditions['cus_id'])."'"; + } + + if(isset($conditions['province']) && $conditions['province']) { + $catCondition[] = " AND province = '".intval($conditions['province'])."'"; + } + + if(isset($conditions['district']) && $conditions['district']) { + $catCondition[] = " AND district = '".intval($conditions['district'])."'"; + } + + if(isset($conditions['folder']) && $conditions['folder']) { + $catCondition[] = " AND `folder` = '". preg_replace("/[^a-z0-9_\-\.]/i", "", $conditions['folder'])."' "; + } + + if(isset($conditions['payment']) && array_key_exists($conditions['payment'], OrderStatus::PAYMENT_STATUS ) ) { + $catCondition[] = " AND `payment_status` = '". $this->db->escape($conditions['payment'])."' "; + } + + if(isset($conditions['fullfillment']) && array_key_exists($conditions['fullfillment'], OrderStatus::FULFILLMENT_STATUS) ) { + $catCondition[] = " AND `fulfillment_status` = '". $this->db->escape($conditions['fullfillment'])."' "; + } + + if(isset($conditions['status']) && array_key_exists($conditions['status'], OrderStatus::ORDER_STATUS) ) { + $catCondition[] = " AND `order_status` = '". $this->db->escape($conditions['status'])."' "; + } + + if(isset($conditions['view_status']) && $conditions['view_status']) { + if($conditions['view_status'] == 'no-status') { + $catCondition[] = " AND `status_id` = 0 "; + } else { + $catCondition[] = " AND `status_id` = '". intval($conditions['view_status'])."' "; + } + } + + if(isset($conditions['update_by']) && $conditions['update_by']) { + $catCondition[] = " AND `status_update_by` = '". $this->db->escape($conditions['update_by'])."' "; + } + + if(isset($conditions['shipping_status']) && $conditions['shipping_status']) { + $catCondition[] = " AND `admin_shipping_status` = '". preg_replace("/[^a-z0-9_\-\.]/i", "", $conditions['shipping_status'])."' "; + } + + if(isset($conditions['assign_to']) && $conditions['assign_to']) { + $catCondition[] = " AND `assign_to` = '". intval($conditions['assign_to'])."' "; + } + + //filter by date + if(isset($conditions['from_date']) && $conditions['from_date']) { + $catCondition[] = " AND order_time >= '". intval(strtotime(TimeManager::convert_date_from_javascript($conditions['from_date'])." 00:00"))."' "; + } + + if(isset($conditions['to_date']) && $conditions['to_date']) { + $catCondition[] = " AND order_time <= '".strtotime(TimeManager::convert_date_from_javascript($conditions['to_date'])." 23:59")."' "; + } + + //filter by hour + if(isset($conditions['from_hour']) && $conditions['from_hour']) { + $catCondition[] = " AND `order_hour` >= '".intval($conditions['from_hour'])."' "; + } + if(isset($conditions['to_hour']) && $conditions['to_hour']) { + $catCondition[] = " AND `order_hour` <= '".intval($conditions['to_hour'])."' "; + } + + if(isset($conditions["excluded_ids"]) && $conditions["excluded_ids"] ){ + $list_ids = filterNumber(explode(",", $conditions["excluded_ids"])); + if(sizeof($list_ids)) $catCondition[] = " AND `orderId` NOT IN (".join(',', $list_ids ).") "; + } + + if(isset($conditions["included_ids"]) && $conditions["included_ids"] ){ + $list_ids = filterNumber(explode(",", $conditions["included_ids"])); + if(sizeof($list_ids)) $catCondition[] = " AND `orderId` IN (".join(',', $list_ids ).") "; + } + + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + $info = $input_info; + + if(isset($info['file_external_url'])) { + if($info['file_external_url'] && !Url::isUrlValid($info['file_external_url'])) { + $info['file_external_url'] = ''; + } + } + + + $info['create_time'] = CURRENT_TIME; + $info['create_by'] = ADMIN_NAME; + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + + protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info) : AppResponse + { + $info = $new_input_info; + + unset($info['id']); + + if(isset($info['file_external_url']) && $info['file_external_url'] && !Url::isUrlValid($info['file_external_url'])) { + $info['file_external_url'] = ''; + } + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + protected function beforeDeleteItem($item_id, $item_info) : AppResponse + { + return new AppResponse('ok'); + } + + protected function afterDeleteItem($item_id, $item_info) + { + + } + + + protected function afterCreateItem($new_item_id, $new_item_info) + { + + } + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) + { + + } + +} diff --git a/inc/Hura8/Components/Order/Model/OrderSearchModel.php b/inc/Hura8/Components/Order/Model/OrderSearchModel.php new file mode 100644 index 0000000..305670b --- /dev/null +++ b/inc/Hura8/Components/Order/Model/OrderSearchModel.php @@ -0,0 +1,30 @@ + "tb_order.price", + ]; + + private $fulltext_fields = [ + "keywords" => ["tb_order.buyerName", ], + ]; + + + public function __construct() + { + parent::__construct( + "tb_order", + $this->fulltext_fields, + $this->filter_fields + ); + } + +} diff --git a/inc/Hura8/Components/Order/Model/OrderStatusModel.php b/inc/Hura8/Components/Order/Model/OrderStatusModel.php new file mode 100644 index 0000000..aae45f0 --- /dev/null +++ b/inc/Hura8/Components/Order/Model/OrderStatusModel.php @@ -0,0 +1,94 @@ +db = get_db('', ENABLE_DB_DEBUG); + } + + public function getStatusHistory($orderId){ + $query = $this->db->runQuery(" + select * from ".$this->tb_status_history." + WHERE order_id = ? + order by id desc + limit 100 + ", ['d'], [ $orderId ] ) ; + + return $this->db->fetchAll($query); + } + + public function createHistory($order_id, $status_type = 'order', $system_status = '', $comment = '', array $data= [], array $other_info = []) { + + $info = $other_info; + + $info['order_id'] = $order_id; + $info['status_type'] = $status_type; + $info['system_status'] = $system_status; + $info['data'] = \json_encode($data); + $info['comment'] = substr($comment, 0, 150); + $info['create_time'] = CURRENT_TIME; + $info['create_by'] = (defined('ADMIN_NAME')) ? ADMIN_NAME : ''; + + return $this->db->insert($this->tb_status_history, $info); + } + + public function getCustomerCancelStatusId() { + $info = $this->db->select($this->tb_status, ['id'], [ + "system_status" => ["=", "cancel"], + 'message' => 'Khách hàng hủy', + ], '', 1); + + if($info) { + return $info['id']; + } + + // create and return + return $this->create([ + 'message' => 'Khách hàng hủy', + 'system_status' => "cancel", + 'create_time' => CURRENT_TIME, + 'create_by' => 'System', + ]); + } + + + public function getInfo($status_id) { + return $this->db->getItemInfo($this->tb_status, $status_id, 'id'); + } + + public function getAll() { + $query = $this->db->runQuery(" select * from ".$this->tb_status." order by `id` desc ") ; + $item_list = array(); + foreach ( $this->db->fetchAll($query) as $rs ) { + if(!$rs['system_status']) $rs['system_status'] = 'new'; + //$rs['system_order_status'] = System::$ORDER_STATUS[$rs['system_status']]; + $item_list[] = $rs; + } + + return $item_list; + } + + public function delete($id) { + return $this->db->runQuery("DELETE FROM ".$this->tb_status." WHERE `id` = ? limit 1 ", ['d'], [ $id ]) ; + } + + public function update($id, array $info) { + return $this->db->update($this->tb_status, $info, ['id' => $id]); + } + + public function create(array $info) { + return $this->db->insert($this->tb_status, $info); + } + +} diff --git a/inc/Hura8/Components/Page/AdminController/APageController.php b/inc/Hura8/Components/Page/AdminController/APageController.php new file mode 100644 index 0000000..48b10ff --- /dev/null +++ b/inc/Hura8/Components/Page/AdminController/APageController.php @@ -0,0 +1,50 @@ +objPageModel->updateTableInfo($item_id, $new_item_info); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // delete thumb files + $item_info = $this->getInfo($item_id); + $this->deleteThumbnailFile($item_info['thumbnail']); + + //delete media files? + // todo: + + return true; + } + + + protected function deleteThumbnailFile($file_name): bool + { + if(!$file_name) { + return false; + } + + foreach (self::$resized_sizes as $size => $value) { + $file_local_path = PUBLIC_DIR . "/". self::$image_folder . "/". $size. IMAGE_FILE_SEPARATOR . $file_name; + unlink($file_local_path); + } + + // remove original file + $file_local_path = PUBLIC_DIR . "/". self::$image_folder . "/". $file_name; + return unlink($file_local_path); + } + +} diff --git a/inc/Hura8/Components/Page/Controller/bPageController.php b/inc/Hura8/Components/Page/Controller/bPageController.php new file mode 100644 index 0000000..00a3629 --- /dev/null +++ b/inc/Hura8/Components/Page/Controller/bPageController.php @@ -0,0 +1,92 @@ + ['width' => 200,] , + 'l' => ['width' => 600,] , + ); + + + protected $objPageModel; + + + public function __construct() + { + $this->objPageModel = new PageModel(); + + if(!$this->isDefaultLanguage()) { + //$this->objPageLanguageModel->createTableLang(); + parent::__construct($this->objPageModel, new PageLanguageModel()); + }else{ + parent::__construct($this->objPageModel); + } + } + + + // get full info- basic with description + public function getFullInfo($id) : ?array + { + if(!$id) return null; + + return self::getCache("getFullInfo-".$id."-".$this->view_language, function () use ($id){ + + $info = $this->objPageModel->getFullInfo($id); + + if($this->iEntityLanguageModel && $info ) { + $item_language_info = $this->iEntityLanguageModel->getInfo($id); + if($item_language_info) { + return $this->formatItemInfo(array_merge($info, $item_language_info)); + } + } + + return $this->formatItemInfo($info); + + }); + } + + + protected function formatItemInfo(array $item_info) + { + if(!$item_info) return null; + + $info = $item_info; + $info['image'] = self::getResizedImageCollection($info['thumbnail']); + + return $info; + } + + + protected function formatItemInList(array $item_info) + { + return $this->formatItemInfo($item_info); + } + + + public static function getResizedImageCollection($image_name) { + $image = []; + + $size_in_full = [ + 't' => 'thumb' , + 's' => 'small' , + 'l' => 'large' , + ]; + + foreach (static::$resized_sizes as $size => $value) { + $image[$size_in_full[$size]] = ($image_name) ? STATIC_DOMAIN . "/". static::$image_folder . "/". $size. IMAGE_FILE_SEPARATOR . $image_name : ''; + } + + return $image; + } + +} diff --git a/inc/Hura8/Components/Page/Model/PageLanguageModel.php b/inc/Hura8/Components/Page/Model/PageLanguageModel.php new file mode 100644 index 0000000..5098c36 --- /dev/null +++ b/inc/Hura8/Components/Page/Model/PageLanguageModel.php @@ -0,0 +1,21 @@ +richtext_fields); + } + + +} diff --git a/inc/Hura8/Components/Page/Model/PageModel.php b/inc/Hura8/Components/Page/Model/PageModel.php new file mode 100644 index 0000000..9f72300 --- /dev/null +++ b/inc/Hura8/Components/Page/Model/PageModel.php @@ -0,0 +1,76 @@ +richtext_fields); + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function getFullInfo($id) : ?array + { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_entity."` basic, `".$this->tb_page_info."` info + WHERE basic.`id` = info.`page_id` AND basic.id = ? + LIMIT 1 ", + ['d'], [$id] + ); + + if( $item_info = $this->db->fetchAssoc($query)){ + return $item_info; + } + + return null; + } + + protected function _buildQueryConditionExtend(array $filter_condition): ?array + { + /*$condition = array( + "q" => "", + "status" => 0, + );*/ + + /*$condition = array( + "letter" => "", + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + /*if(isset($filter_condition["letter"]) && strlen($filter_condition["letter"]) == 1){ + $catCondition[] = " AND `letter` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["letter"]; + }*/ + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductAttributeController.php b/inc/Hura8/Components/Product/AdminController/AProductAttributeController.php new file mode 100644 index 0000000..c4eb804 --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductAttributeController.php @@ -0,0 +1,165 @@ +objProductAttributeModel = new ProductAttributeModel(); + + if(!$this->isDefaultLanguage()) { + $this->objProductAttributeLanguageModel = new ProductAttributeLanguageModel($this->view_language); + //$this->objProductAttributeLanguageModel->createTableLang(); + + parent::__construct($this->objProductAttributeModel, $this->objProductAttributeLanguageModel); + + }else{ + parent::__construct($this->objProductAttributeModel); + } + } + + + public function getProductAttributes($product_id) { + $objProductAttributeValueModel = new ProductAttributeValueModel(0); + + $result = []; + foreach ($objProductAttributeValueModel->getProductAttributes($product_id) as $info) { + $result[$info['attr_id']][] = $info['attr_value_id']; + } + + return $result; + } + + public function updateProductAttributes($product_id, array $current_value, array $new_value) { + /* + $current_value => Array + ( + // attribute_id => [value1, value2] + [2] => Array + ( + [0] => 2 + [1] => 3 + ) + + [1] => Array + ( + [0] => 1 + ) + + ) + + $new_value => Array + ( + // attribute_id => new_str_values by lines + [2] => asdas + [1] => asda + ) + * */ + + // list of values for product + $product_value_ids = []; + + // use current values + foreach ($current_value as $_attr_id => $_value_ids) { + $product_value_ids = array_merge($product_value_ids, $_value_ids); + } + + // create new values for attributes + foreach ($new_value as $_attr_id => $_value_text) { + $new_value_texts = array_filter(explode("\n", $_value_text)); + foreach ($new_value_texts as $new_value) { + // if exist + $check_exist = $this->getValueByTitle($_attr_id, $new_value); + if($check_exist) { + $product_value_ids[] = $check_exist['id']; + }else{ + $try_create_id = $this->addValue($_attr_id, ['title' => $new_value]); + if($try_create_id) $product_value_ids[] = $try_create_id; + } + } + } + + + $objProductAttributeValueModel = new ProductAttributeValueModel(0); + + return $objProductAttributeValueModel->updateProductAttributes($product_id, $product_value_ids); + + } + + + public function updateAttributeInSpecGroup($group_id, $attr_id, array $info) { + $this->objProductAttributeModel->updateAttributeInSpecGroup($group_id, $attr_id, $info); + } + + public function removeAttributeFromSpecGroup($group_id, $attr_id) { + $this->objProductAttributeModel->removeAttributeFromSpecGroup($group_id, $attr_id); + } + + public function addAttributeToSpecGroup($group_id, $attr_id, $ordering) { + $this->objProductAttributeModel->addAttributeToSpecGroup($group_id, $attr_id, $ordering); + } + + public function updateAttributeInCategory($cat_id, $attr_id, array $info) { + $this->objProductAttributeModel->updateAttributeInCategory($cat_id, $attr_id, $info); + } + + public function removeAttributeFromCategory($cat_id, $attr_id) { + $this->objProductAttributeModel->removeAttributeFromCategory($cat_id, $attr_id); + } + + public function addAttributeToCategory($cat_id, $attr_id, $ordering) { + $this->objProductAttributeModel->addAttributeToCategory($cat_id, $attr_id, $ordering); + } + + public function getAllAtributes() { + return $this->objProductAttributeModel->getList(["numPerPage" => 2000]); + } + + public function deleteValue($attr_id, $value_id) { + $objProductAttributeValueModel = new ProductAttributeValueModel($attr_id); + return $objProductAttributeValueModel->delete($value_id); + } + + public function updateValue($attr_id, $value_id, array $info) { + $objProductAttributeValueModel = new ProductAttributeValueModel($attr_id); + return $objProductAttributeValueModel->updateFields($value_id, $info); + } + + public function getValueByTitle($attr_id, $value_title) { + $objProductAttributeValueModel = new ProductAttributeValueModel($attr_id); + $filter_code = DataClean::makeInputSafe($value_title, DataType::ID); + return $objProductAttributeValueModel->getInfoByCode($filter_code); + } + + public function addValue($attr_id, array $info) { + $objProductAttributeValueModel = new ProductAttributeValueModel($attr_id); + return $objProductAttributeValueModel->create($info); + } + + public function getListAttributeValues($attr_id) { + $objProductAttributeValueModel = new ProductAttributeValueModel($attr_id); + return $objProductAttributeValueModel->getList(['numPerPage' => 200]); + } + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + return true; + } +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductCategoryController.php b/inc/Hura8/Components/Product/AdminController/AProductCategoryController.php new file mode 100644 index 0000000..8e7da59 --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductCategoryController.php @@ -0,0 +1,18 @@ +objProductCategoryModel->getAttributeList($catId); + } + +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductCollectionController.php b/inc/Hura8/Components/Product/AdminController/AProductCollectionController.php new file mode 100644 index 0000000..6e727b5 --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductCollectionController.php @@ -0,0 +1,33 @@ +objProductCollectionModel->updateProduct($product_id, $collection_id, $info); + } + + + public function removeProduct($product_id, $collection_id) + { + return $this->objProductCollectionModel->removeProduct($product_id, $collection_id); + } + + + public function addProduct($product_id, $collection_id, $ordering=0) + { + return $this->objProductCollectionModel->addProduct($product_id, $collection_id, $ordering); + } + + +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductController.php b/inc/Hura8/Components/Product/AdminController/AProductController.php new file mode 100644 index 0000000..7de1ba8 --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductController.php @@ -0,0 +1,73 @@ + array('max' => 0, 'min'=> 0), + "brand" => array(), // array(1,2,3,) + "collection" => array(), // array(1,2,3,) + "supplier" => array(), // array(1,2,3,) + "rating" => array(), // array(1,2,3,) + "category" => array(), // array(1,2,3,) + "status" => array(), // array(1,2,3,) + "hotType" => array(),// array(saleoff | not | new) + "attribute" => array(), // array(1,2,3,) + "promotion" => "", + "storeId" => array(), // array(1,2,3,) + "other_filter" => array(), // array(in-stock, has-promotion etc...) + "spec_group_id" => array(), // array(1,2,3,) + ]; + } + + //get product image list + public function productImageList($proId){ + $objProductImageModel = new ProductImageModel($proId); + $result = array(); + foreach ( $objProductImageModel->getList(["numPerPage" => 100]) as $rs ) { + + $rs['image'] = static::getResizedImageCollection($rs['img_name']); + + $result[] = $rs; + } + + return $result; + } + + // get only from tb_product_info + public function getInfoMore($id) + { + $objProductInfoModel = new ProductInfoModel(); + $info = $objProductInfoModel->getInfo($id); + + if(!$this->isDefaultLanguage() && $info) { + $language_info = $this->iEntityLanguageModel->getInfo($id); + $final_info = []; + foreach ($info as $_k => $_v) { + $final_info[$_k] = $language_info[$_k] ?? $_v; + } + + return $final_info; + } + + return $info; + } + + public function getInfoMoreEmpty($addition_field_value = []) + { + $objProductInfoModel = new ProductInfoModel(); + return $objProductInfoModel->getEmptyInfo($addition_field_value); + } + +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductFilterController.php b/inc/Hura8/Components/Product/AdminController/AProductFilterController.php new file mode 100644 index 0000000..5bb29af --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductFilterController.php @@ -0,0 +1,130 @@ + '', + "name" => "Sắp xếp sản phẩm", + "url" => Url::buildUrl(CURRENT_URL, array("order"=>"")), + ), + array( + "selected" => ($order == 'ordering') ? "selected" : "", + "name" => "Thứ tự cửa hàng", + "url" => Url::buildUrl(CURRENT_URL, array("order"=>"ordering")), + ), + array( + "selected" => ($order == 'view') ? "selected" : "", + "name" => "Xem nhiều nhất", + "url" => Url::buildUrl(CURRENT_URL, array("order"=>"view")), + ), + array( + "selected" => ($order == 'new') ? "selected" : "", + "name" => "Mới nhất", + "url" => Url::buildUrl(CURRENT_URL, array("order"=>"new")), + ), + array( + "selected" => ($order == 'last-update') ? "selected" : "", + "name" => "Thời gian cập nhật", + "url" => Url::buildUrl(CURRENT_URL, array("order"=>"last-update")), + ), + ); + } + + + public static function getFilterOptions($other_filter) { + + return array( + array( + "selected" => ($other_filter == 'no-price') ? "selected" : "", + "name" => "Giá bán = 0", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"no-price")), + ), + array( + "selected" => ($other_filter == 'in-stock') ? "selected" : "", + "name" => "Còn hàng", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"in-stock")), + ), + array( + "selected" => ($other_filter == 'out-stock') ? "selected" : "", + "name" => "Hết hàng", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"out-stock")), + ), + array( + "selected" => ($other_filter == 'has-market-price') ? "selected" : "", + "name" => "Có giá thị trường", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"has-market-price")), + ), + + array( + "selected" => ($other_filter == 'has-promotion') ? "selected" : "", + "name" => "Có khuyến mại", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"has-promotion")), + ), + + array( + "selected" => ($other_filter == 'has-config') ? "selected" : "", + "name" => "Có cấu hình", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"has-config")), + ), + + array( + "selected" => ($other_filter == 'no-sku') ? "selected" : "", + "name" => "Chưa có mã kho", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"no-sku")), + ), + array( + "selected" => ($other_filter == 'no-image') ? "selected" : "", + "name" => "Chưa có ảnh", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"no-image")), + ), + array( + "selected" => ($other_filter == 'display-off') ? "selected" : "", + "name" => "Chưa hiển thị", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"display-off")), + ), + array( + "selected" => ($other_filter == 'display-on') ? "selected" : "", + "name" => "Đang hiển thị", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"display-on")), + ), + array( + "selected" => ($other_filter == 'no-category') ? "selected" : "", + "name" => "Chưa có danh mục", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"no-category")), + ), + array( + "selected" => ($other_filter == 'no-brand') ? "selected" : "", + "name" => "Chưa có thương hiệu", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"no-brand")), + ), + + array( + "selected" => ($other_filter == 'no-warranty') ? "selected" : "", + "name" => "Chưa có bảo hành", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"no-warranty")), + ), + array( + "selected" => ($other_filter == 'no-description') ? "selected" : "", + "name" => "Chưa có mô tả", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"no-description")), + ), + array( + "selected" => ($other_filter == 'no-spec-text') ? "selected" : "", + "name" => "Chưa có thông số nhập text", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"no-spec-text")), + ), + ); + + } + +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductFilterOldController.php b/inc/Hura8/Components/Product/AdminController/AProductFilterOldController.php new file mode 100644 index 0000000..6a4c63c --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductFilterOldController.php @@ -0,0 +1,948 @@ + false, + "brand" => array(), + "collection" => array(), + "supplier" => array(), + "rating" => array(), + "category" => array(), + "status" => "", + "query" => "", + "hotType" => "",//saleoff | not | new + "attribute" => [],//,3793,3794, + "ids" => array(), + 'excluded_ids' => array(), + "promotion" => "", + "storeId" => '', + "other_filter" => array() //in-stock, has-promotion etc... + ]; + + + public function __construct() { + + } + + + public function getDefaultFilter() { + return [ + "price" => self::getPriceFilterRange(), + "brand" => $this->getBrandFilter(), + "collection" => array_filter(explode(FILTER_VALUE_SEPARATOR, getRequest('collection'))), + "supplier" => array_filter(explode(FILTER_VALUE_SEPARATOR, getRequest('supplier'))), + "rating" => array_filter(explode(FILTER_VALUE_SEPARATOR, getRequest('rating'))), + "category" => array_filter(explode(FILTER_VALUE_SEPARATOR, getRequest('category'))), + //"status" => "1", + "query" => getRequest('q', ''), + "hotType" => getRequest('hotType'),//saleoff | not | new + "attribute" => $this->getAttributeFilter([]),//,3793,3794, + "promotion" => getRequest('promo', ''), + "ids" => array_filter(explode(',', str_replace(" ", "", getRequest('ids', '')))), + 'excluded_ids' => [], + "storeId" => getRequest('storeId', ''), + "other_filter" => array_filter(explode(",", getRequest('other_filter', ''))), //in-stock, has-promotion etc... + ]; + } + + + // build the user filters for product query + // we can modify the code to accept various types of url customization + public function getUserFilter(array $category_list_id) { + return [ + "price" => self::getPriceFilterRange(), + "brand" => $this->getBrandFilter(), + "collection" => array_filter(explode(FILTER_VALUE_SEPARATOR, getRequest('collection'))), + "supplier" => array_filter(explode(FILTER_VALUE_SEPARATOR, getRequest('supplier'))), + "rating" => array_filter(explode(FILTER_VALUE_SEPARATOR, getRequest('rating'))), + "category" => $category_list_id, + //"status" => "1", + "query" => getRequest('q', ''), + "hotType" => getRequest('hotType'),//saleoff | not | new + "attribute" => $this->getAttributeFilter( $category_list_id),//,3793,3794, + "promotion" => getRequest('promo', ''), + "ids" => array_filter(explode(',', str_replace(" ", "", getRequest('ids', '')))), + "excluded_ids" => array_filter(explode(',', str_replace(" ", "", getRequest('excluded_ids', '')))), + "storeId" => getRequest('storeId', ''), + "other_filter" => array_filter(explode(",", getRequest('other_filter', ''))), //in-stock, has-promotion etc... + ]; + } + + + // allow to update filters + public function setFilters(array $new_filters) { + foreach ($new_filters as $key => $value) { + if(isset($this->filters[$key])) { + $this->filters[$key] = $value; + } + } + } + + + // accept url format: + // default: ?min=1000&max=20000 + // custom: ?p=15trieu-20-trieu + public static function getPriceFilterRange(){ + // default format + if(!defined('PRICE_FILTER_FORMAT') || PRICE_FILTER_FORMAT != 'p') { + return array( + "min" => getRequestInt("min", 0), + "max" => getRequestInt("max", 0), + ); + } + + // custom price range query + $price_range_format = getRequest("p"); // duoi-10trieu , 10ngan-2trieu, 3trieu-6trieu, tren-30trieu + if(strpos($price_range_format, '-') === false) { + return array( + "min" => 0, + "max" => 0, + ); + } + + $unit_translation = [ + 'ngan' => 1000, + 'trieu' => 1000000, + 'ty' => 1000000000, + ]; + + // duoi-10trieu, duoi-10ngan + if(strpos($price_range_format, 'duoi-') !== false) { + $unit_match = self::findPriceUnitMatch(str_replace("duoi-", "", $price_range_format)); + return array( + "min" => 0, + "max" => $unit_match['number'] * $unit_translation[$unit_match['unit']], + ); + } + + // tren-10trieu, tren-10ngan + if(strpos($price_range_format, 'tren-') !== false) { + $unit_match = self::findPriceUnitMatch(str_replace("tren-", "", $price_range_format)); + return array( + "min" => $unit_match['number'] * $unit_translation[$unit_match['unit']], + "max" => 0, + ); + } + + // 10ngan-2trieu, 3trieu-6trieu + $parts = explode('-', $price_range_format); + $min_part = $parts[0]; + $max_part = $parts[1]; + $min_match = self::findPriceUnitMatch($min_part); + $max_match = self::findPriceUnitMatch($max_part); + + return [ + "min" => $min_match['number'] * $unit_translation[$min_match['unit']], + "max" => $max_match['number'] * $unit_translation[$max_match['unit']], + ]; + } + + + //get a list of products that match filtering conditions + public function getProductList( + $start_url, + $sort_by = "new", + $limit = 20, + $page = 1 + ) { + //list of available filters + /* + * - price range + * - brand id or ids + * - collection id or ids + * - supplier id + * - rating: 1-> 5 + * - category id or ids + * - status: 0|1|null + * - detail_page_only: 0 | 1 + * - query: search keyword + * - hotType: saleoff | not | new | or combination of types + * - attribute values + * ... + * */ + + /*$filters = array( + "price" => array("min"=> 1, "max" => 100), + "brand" => array(12,3), + "collection" => array(2,3), + "supplier" => array(2,3), + "rating" => array(2,3), + "category" => array(1,2,), + "status" => "1", + "detail_page_only" => 0, + "query" => "", + "hotType" => "",//saleoff | not | new + "attribute" => "",//,3793,3794, + "ids" => array(12,3), + 'excluded_ids' => array(12,3), + "other_filter" => array(in-stock, has-promotion) + //... + );*/ + + $filterPath = []; + $filter_messages = []; + $where_query = []; + $paging_url = $start_url; + $filters = $this->cleanFilter(); + // debug_var($filters); + + //system controls + if(ENABLE_PRODUCT_EXPIRE) { + //$where_query[] = " AND (from_time =0 OR from_time < '".CURRENT_TIME."') AND (to_time > '".CURRENT_TIME."' OR to_time=0 ) "; + } + + //other filters + if(isset($filters["other_filter"]) && sizeof($filters["other_filter"])) { + foreach ($filters["other_filter"] as $_filter) { + switch ($_filter) { + case "in-stock"; + $filter_messages[] = [ + 'title' => 'Còn hàng', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND `quantity` > 0 "; + break; + case "has-vat"; + $filter_messages[] = [ + 'title' => 'Có thuế VAT', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND has_vat = 1 "; + break; + case "out-stock"; + $filter_messages[] = [ + 'title' => 'Hết hàng', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND quantity = 0 "; + break; + case "has-market-price"; + $filter_messages[] = [ + 'title' => 'Có giá thị trường', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND market_price > 0 "; + break; + case "no-price"; + $filter_messages[] = [ + 'title' => 'Không có giá', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND price = 0 "; + break; + case "no-warranty"; + $filter_messages[] = [ + 'title' => 'Không có bảo hành', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND LENGTH(`warranty`) < 2 "; + break; + case "no-sku"; + $filter_messages[] = [ + 'title' => 'Không mã kho hàng', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND LENGTH(`sku`) < 2 "; + break; + case "has-sku"; + $filter_messages[] = [ + 'title' => 'Có mã kho hàng', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND LENGTH(`sku`) > 2 "; + break; + case "has-config"; + $filter_messages[] = [ + 'title' => 'Không có cấu hình', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND `config_count` > 0 "; + break; + case "no-image"; + $filter_messages[] = [ + 'title' => 'Không có ảnh', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND `image_count` = 0 "; + break; + case "no-category"; + $where_query[] = " AND `category_ids` = '' "; + break; + case "no-brand"; + $filter_messages[] = [ + 'title' => 'Không có thương hiệu', + 'reset' => Url::buildUrl(CURRENT_URL, ['brand' => '']), + ]; + + $where_query[] = " AND `brand_id` = 0 "; + break; + case "display-off"; + $where_query[] = " AND `status`=0 "; + break; + case "display-on"; + $where_query[] = " AND `status`=1 "; + break; + case "has-promotion": + $filter_messages[] = [ + 'title' => 'Có khuyến mại', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + + $where_query[] = " AND LENGTH(`special_offer`) > 5 "; + break; + + case "no-description"; + $filter_messages[] = [ + 'title' => 'Không có mô tả', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + + $where_query["no-description"] = " AND `id` IN ( SELECT `id` FROM ".TableName::PRODUCT_INFO." WHERE LENGTH(`description`) < 5 ) "; + break; + + case "no-spec-text"; + $filter_messages[] = [ + 'title' => 'Không có thông số', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + + $where_query["no-spec-text"] = " AND `id` IN ( SELECT `id` FROM ".TableName::PRODUCT_INFO." WHERE LENGTH(`spec`) < 5 ) "; + break; + + } + } + } + + + + //- brand id or ids or brand_indexes + if(isset($filters["brand"]) && sizeof($filters["brand"])) { + global $admin_panel; + + $brand_url_format = (defined('BRAND_FILTER_FORMAT')) ? BRAND_FILTER_FORMAT : ''; + if(isset($admin_panel) && $admin_panel) $brand_url_format = ''; // custom brand format cannot be used in admin panel + + $objBrandModel = new BrandModel(); + $condition = array(); + foreach ($filters["brand"] as $_id) { + if(!$_id) continue; + + $brand_info = $objBrandModel->getInfo($_id); + if(!$brand_info) continue; + + $filterPath["brand"][] = array( + "id" => $brand_info['id'], + "name" => $brand_info["title"], + ); + + $condition[] = " `brand_id` = '".intval($brand_info['id'])."' "; + $filter_messages[] = [ + 'title' => $brand_info['title'], + 'reset' => Url::buildUrl(CURRENT_URL, ['brand' => join(FILTER_VALUE_SEPARATOR, remove_item_from_array($filters["brand"], $_id))]), + ]; + } + + if(sizeof($condition)) { + $paging_url = Url::buildUrl($paging_url, array("brand" => join(FILTER_VALUE_SEPARATOR, $filters['brand']))); + $where_query[] = " AND ( ".join(" OR ", $condition)." )"; + } + + } + + //- collection id or ids + if(isset($filters["collection"]) && sizeof($filters["collection"])) { + $condition = array(); + foreach ($filters["collection"] as $_id) { + $filterPath["collection"][] = array( + "id" => $_id, + //"name" => $brand_info["name"], + ); + $condition[] = " `id` IN ( SELECT product_id FROM ".TB_CATEGORY_SPECIAL_PRODUCT." WHERE `special_cat_id`='".intval($_id)."' ) "; + $filter_messages[] = [ + 'title' => 'Bộ sưu tập ', + 'reset' => Url::buildUrl(CURRENT_URL, ['collection' => join(',', remove_item_from_array($filters["collection"], $_id))]), + ]; + } + + $paging_url = Url::buildUrl($paging_url, array("collection" => join(",", $filters['collection']))); + $where_query[] = " AND ( ".join(" OR ", $condition)." )"; + } + + + //- category id or ids + if(isset($filters["category"]) && sizeof($filters["category"])) { + + $objCategoryProductModel = new ProductCategoryModel(); + + $condition = array(); + + foreach ($filters["category"] as $cat_id) { + $cat_id = intval($cat_id); + + if(!$cat_id) continue; + + $cat_info = $objCategoryProductModel->getInfo($cat_id); + + if($cat_info["is_parent"]) { + $childListId = ($cat_info["child_ids"]) ? $cat_info["child_ids"] : '0'; + $condition[] = " `category_id` IN (".$childListId .") "; + }else{ + $condition[] = " `category_id` = '".$cat_id."' "; + } + + $filterPath["category"][] = array( + "id" => $cat_id, + "name" => $cat_info['title'], + ); + + $filter_messages[] = [ + 'title' => $cat_info['title'], + 'reset' => Url::buildUrl(CURRENT_URL, ['category' => join(',', remove_item_from_array($filters["category"], $cat_id))]), + ]; + } + + if(sizeof($condition)) { + $paging_url = Url::buildUrl($paging_url, array("category" => join(",", $filters['category']))); + $where_query[] = " AND `id` IN ( SELECT DISTINCT `item_id` FROM ".TableName::PRODUCT_PER_CATEGORY." WHERE " . join(" OR ", $condition) . " )"; + } + } + + //- status: 0|1|null + if(isset($filters["status"]) && $filters['status']) { + $where_query[] = " AND `status` = 1 "; + /*$filter_messages[] = [ + 'title' => 'Trạng thái: '.($filters['status'] ? 'Đang hiển thị' : 'Đang ẩn'), + 'reset' => Url::buildUrl(CURRENT_URL, ['status' => '']), + ];*/ + } + + + //- query: search keyword + if(isset($filters["query"]) && $filters["query"]) { + $keyword_search = $filters["query"]; + $search_by_product_id = intval(preg_replace('/[^0-9]/i', '', $keyword_search)); + + $objProductSearchModel = new ProductSearchModel(); + + $match_result = $objProductSearchModel->find($keyword_search); + + $filterPath["search"] = array( + "id" => $filters["query"], + "name" => $filters["query"], + ); + $filter_messages[] = [ + 'title' => $filters["query"], + 'reset' => Url::buildUrl(CURRENT_URL, ['q' => '']), + ]; + + $paging_url = Url::buildUrl($paging_url, array("q" => $filters["query"])); + + if(sizeof($match_result) > 0) { + $where_query[] = " AND ( `id` IN (".join(",", $match_result ).") OR `id` = '".$search_by_product_id."' ) "; + }else{ + $where_query[] = " AND `id` = '".$search_by_product_id."' "; + } + + + } + + //- hotType: saleoff | not | new | or combination of types + if(isset($filters["hotType"]) && $filters["hotType"]) { + $hot_type = preg_replace("/[^a-z0-9_\-]/i","", $filters["hotType"]); + $config_hottype = AProductFilterController::getProductHotTypeList(); + if(isset($config_hottype[$hot_type])) { + $filterPath["hotType"] = array( + "id" => $filters["hotType"], + "name" => $filters["hotType"], + ); + $paging_url = Url::buildUrl($paging_url, array("hotType" => $hot_type)); + $where_query[] = " AND `id` IN (SELECT `pro_id` FROM ".TB_PRODUCT_HOT." WHERE hot_type = '".$hot_type."' ) "; + $filter_messages[] = [ + 'title' => $filters["hotType"], + 'reset' => Url::buildUrl(CURRENT_URL, ['hotType' => '']), + ]; + } + } + + //- attribute values + if(isset($filters["attribute"]) && sizeof($filters["attribute"])) { + $filter_attr_value_list = $filters["attribute"]; + //filter = attr_value_1-attr_value_2-attr_value_3, + $query_attr_id = []; + $count_filter = 0; + + if(ENABLE_FILTER_BY_APIKEY) { + //filter = ,api_key_1,api_key_2,api_key_3, + foreach($this->translate_api_filter($filter_attr_value_list) as $attr_id){ + $query_attr_id[] = $attr_id; + $count_filter ++; + } + + } else { + foreach($filter_attr_value_list as $attr_id){ + $attr_id = (int) $attr_id; + if($attr_id) { + $query_attr_id[] = $attr_id; + $count_filter ++; + } + } + } + + $objProductAttributeModel = new ProductAttributeModel(); + + $product_filter_id_match = $objProductAttributeModel->getProductMatchAttributeValue($query_attr_id); + + + $paging_url = Url::buildUrl($paging_url, array("filter" => join(FILTER_VALUE_SEPARATOR, $filter_attr_value_list))); + $where_query[] = (sizeof($product_filter_id_match)) ? " AND `id` IN (".join(', ', $product_filter_id_match).") " : " AND `id` = 0 " ; + + //xay lai url de back + foreach($filter_attr_value_list as $value_id ){ + $att_name = 'att_name'; //$objCategoryProduct->atrValueName($value_id); + $filterPath["attribute"][] = array( + "id" => $value_id, + "name" => $att_name, + ); + $filter_messages[] = [ + 'title' => $att_name, + 'reset' => Url::buildUrl(CURRENT_URL, ['attribute' => join(FILTER_VALUE_SEPARATOR, remove_item_from_array($filters["category"], $value_id))]), + ]; + } + } + + //given products' ids + if(isset($filters["ids"]) && is_array($filters["ids"]) && sizeof($filters["ids"])) { + $where_query[] = " AND `id` IN (". join(",", $filters["ids"]) .") "; + } + + //exclude products' ids + if(isset($filters["excluded_ids"]) && is_array($filters["excluded_ids"]) && sizeof($filters["excluded_ids"])) { + $where_query[] = " AND `id` NOT IN (". join(",", $filters["excluded_ids"]) .") "; + } + + $price_range_query_limit = join(" ",$where_query); + + //- price range + if(isset($filters["price"]) && is_array($filters["price"]) && sizeof($filters["price"]) && ($filters["price"]['min'] > 0 || $filters["price"]['max'] > 0)) { + //limit by price range + $maxPrice = clean_price($filters["price"]['max']); + $minPrice = clean_price($filters["price"]['min']); + $paging_url = Url::buildUrl($paging_url, array("max"=>$maxPrice, "min"=>$minPrice)); + + $price_range_query = ''; + if($maxPrice > 0 && $minPrice > 0){ + $price_range_query = " ( `price` BETWEEN '".$minPrice."' AND '".$maxPrice."' ) "; + + }else if($maxPrice > 0){ + $price_range_query = " `price` < '".$maxPrice."' "; + + }else if($minPrice > 0){ + $price_range_query = " `price` >='".$minPrice."' "; + } + + $where_query[] = " AND ". $price_range_query; + + $filterPath["price"] = array( + "min" => $minPrice, + "max" => $maxPrice, + ); + + $price_format = ProductFilterPrice::buildPriceRangeFormat($minPrice, $maxPrice); + + $filter_messages[] = [ + 'title' => $price_format['title'], + 'reset' => Url::buildUrl(CURRENT_URL, ['price' => '']), + ]; + } + + // use location price sorting + $ordering_clause = $this->getOrderingClause($sort_by, $db_condition, 0); + + $db_condition = join(" ", $where_query); + + $total_number = 0; + $list_ids = []; + + /* $query = $this->db->runQuery("SELECT COUNT(`id`) AS total FROM ".TB_PRODUCT_LIGHT." WHERE 1 ".$db_condition." "); + if($rs = $this->db->fetchAssoc($query)) { + $total_number = $rs['total']; + } + + $category_query = " AND idv_product_category.`pro_id` IN (SELECT `id` FROM ".TB_PRODUCT_LIGHT." WHERE 1 ".$db_condition.") "; + + if($location_sorting) { + + $list_ids = array_slice($location_product_ids, ($page-1) * $limit, $limit); + + } else { + + if(in_array($sort_by, ['view', 'name'])) { + $query = $this->db->runQuery(" + SELECT `id`, ".TB_PRODUCT.".proName + FROM ".TB_PRODUCT_LIGHT.", ".TB_PRODUCT." + WHERE `id` = ".TB_PRODUCT.".`id` + ".$db_condition." + ".$ordering_clause." + LIMIT ".($page-1) * $limit.", ".$limit." + "); + }else{ + $query = $this->db->runQuery(" + SELECT `id` FROM ".TB_PRODUCT_LIGHT." + WHERE 1 ".$db_condition." + ".$ordering_clause." + LIMIT ".($page-1) * $limit.", ".$limit." + "); + } + + $list_ids = array_map(function ($rs){ + return $rs['id']; + }, $this->db->fetchAll($query)); + + }*/ + + + return [ + "full_query" => $db_condition, + "price_query" => $price_range_query_limit, + //"category_query" => $category_query, + "filter" => $filterPath, + "filter_messages" => $filter_messages, + "total" => $total_number, + "list_ids" => $list_ids, + "url" => $paging_url, + ]; + + } + + + protected function cleanFilter() { + $clean_filter = []; + foreach ($this->filters as $key => $value) { + $clean_filter[$key] = $this->clearFilterValue($key, $value); + } + return $clean_filter; + } + + + protected function clearFilterValue($key, $value) { + if($key == 'price') { + if(!is_array($value)) return false; + $min = isset($value['min']) ? intval($value['min']) : 0; + $max = isset($value['max']) ? intval($value['max']) : 0; + + return ['min' => $min, 'max' => $max]; // array("min" => getRequestInt("min", 0), "max" => getRequestInt("max", 0)), + } + + if(in_array($key, ['collection', 'supplier', 'rating', 'category', 'ids', 'excluded_ids'])) { + return Security::makeListOfInputSafe($value, 'int'); // int1-int2 => array(int1, int2, ...) + } + + // accept a-z0-9- + if(in_array($key, ['attribute', 'brand'])) { + return Security::makeListOfInputSafe($value, 'string'); + } + + if($key == 'status') { + return (in_array($value, [0, 1])) ? $value : 0; + } + + if($key == 'query') { + return substr(trim(str_replace(['\'', '"'], '', strip_tags($value))), 0, 100); // string + } + + if($key == 'hotType') { + $config_hottype = AProductFilterController::getProductHotTypeList(); + return (isset($config_hottype[$value])) ? $value : ''; //saleoff | not | new + } + + if($key == 'promotion') { + return intval($value); + } + + if($key == 'storeId') { + return intval($value); + } + + if($key == 'detail_page_only') { + return intval($value); // 1|0|'' + } + + if($key == 'other_filter') { + return array_filter( + array_map(function ($item){ + return preg_replace('/[^a-z0-9_\.\-\,]/i', '', $item); + }, $value ) + ); // array() //in-stock, has-promotion + } + + return false; + } + + + protected function filterLocationProductByPrice(array $product_list, $max_price, $min_price) { + if($max_price > 0 && $min_price > 0){ + return array_filter($product_list, function ($pro) use ($max_price, $min_price) { + return ($pro['price'] >= $min_price && $pro['price'] < $max_price); + }); + } + + if($max_price > 0){ + return array_filter($product_list, function ($pro) use ($max_price, $min_price) { + return ($pro['price'] < $max_price); + }); + } + + if($min_price > 0){ + return array_filter($product_list, function ($pro) use ($max_price, $min_price) { + return ($pro['price'] >= $min_price); + }); + } + + return $product_list; + } + + protected function getProductIds($price_range_query_limit) { + /*$query = $this->db->query(" + SELECT `id` FROM ".TB_PRODUCT_LIGHT." + WHERE 1 ".$price_range_query_limit." + LIMIT 10000 + "); + + return array_map(function ($rs){ + return $rs['id']; + }, $this->db->fetchAll($query));*/ + } + + //10-04-2017 allow ?filter=api_key1,api_key2, ... and translate to id + protected function translate_api_filter($filter_api) { + /*$filter_attr_value_list = array_filter(explode(",", $filter_api)); + + $build_query = array(); + foreach($filter_attr_value_list as $api_key){ + $api_key = \preg_replace('/[^a-z0-9\_\-\.]/i', '', $api_key); + if($api_key) $build_query[] = " `api_key` = '".$this->db->escape($api_key)."' "; + } + + $result = array(); + if(sizeof($build_query)) { + $query = $this->db->runQuery("SELECT id FROM ".TB_ATTRIBUTE_VALUE." WHERE ". join(" OR ", $build_query)) ; + foreach ( $this->db->fetchAll($query) as $info ) { + $result[] = $info['id']; + } + } + + return $result;*/ + return ''; + } + + + // accept url format: + // default: ?brand=1 + // brand_index: ?brand=apple + protected function getBrandFilter() { + $brand_url_format = (defined('BRAND_FILTER_FORMAT')) ? BRAND_FILTER_FORMAT : ''; + + $module = (Registry::getVariable('global')) ? Registry::getVariable('global')['module'] : null; + + if(!$module) { + // possible admin panel + return array_filter(explode(FILTER_VALUE_SEPARATOR, getRequest('brand', ''))); + } + + $current_brand_queries = (isset($module['query']['brand'])) ? array_filter(explode(FILTER_VALUE_SEPARATOR, $module['query']['brand'])) : []; + + // fix for brand-detail page + // example: domain.com/brand/sony + if(isset($module['query']['brandName']) && $module['query']['brandName']) { + $objBrandModel = new BrandModel(); + $brand_info = $objBrandModel->getInfoByUrl($module['query']['brandName']); + + if($brand_url_format == 'brand_index') { + $current_brand_queries = array($brand_info['brand_index']); + }else{ + $current_brand_queries = array($brand_info['id']); + } + } + + if($brand_url_format == 'brand_index') { + //i.e ?brand=apple-canon + return $current_brand_queries; + } + + $selected_brand = $current_brand_queries; + if(isset($module['query']['brand_id']) && $module['query']['brand_id']) { + $selected_brand[] = intval($module['query']['brand_id']); + } + + return $selected_brand; + } + + // accept url format: + // default: ?filter=3793-3794, + // custom: ?ram=8gb&hdd=500GB + protected function getAttributeFilter(array $category_list_id){ + + $request_filter_list = getRequest('filter', ''); + + // custom1 + if( !$request_filter_list && defined('ATTRIBUTE_FILTER_FORMAT') && ATTRIBUTE_FILTER_FORMAT == 'custom1' ) { + // iUCategoryProduct $objCategoryProduct, array $category_list_id + return $this->getAttributeFilterPara($category_list_id); //join(FILTER_VALUE_SEPARATOR, $this->getAttributeFilterPara($category_list_id)); + } + + if(!$request_filter_list) { + return []; + } + + // default + $clean_list = Security::makeListOfInputSafe( + explode(FILTER_VALUE_SEPARATOR, str_replace([',', '-', '_'], FILTER_VALUE_SEPARATOR, $request_filter_list) ), + DataType::INTEGER + ) ; //preg_replace("/[^0-9,_\-]/", "", getRequest('filter', ''));//3793-3794, + + return array_filter($clean_list); + } + + + protected function getAttributeFilterPara( array $category_list_id){ + + $excluded_keys = ['p', 'min', 'max', 'brand', 'page', 'request_path']; + $attr_filter_params = []; + + $query_parameters = Url::parse(CURRENT_URL)['query']; + + + foreach ( $query_parameters as $key => $value) { + if(in_array($key, $excluded_keys) || !$value) continue; + + $attr_filter_params[$key] = $value; + } + + + if(sizeof($attr_filter_params)) { + /*$objCategoryProduct = new UCategoryProduct(); + $category_attributes = $objCategoryProduct->getAttributesForCategory($category_list_id); + + $match_attr_value_ids = []; + foreach ($attr_filter_params as $attr_filter_code => $attr_value_api_key) { + foreach ($category_attributes as $attribute) { + if($attribute['filter_code'] != $attr_filter_code) continue; + + foreach ($attribute['value_list'] as $_value_id => $_value_info) { + if($_value_info['info']['api_key'] == $attr_value_api_key) { + $match_attr_value_ids[] = $_value_id; + } + } + } + } + + return $match_attr_value_ids;*/ + return []; + } + + return []; + } + + + public static function findPriceUnitMatch($str){ + $match = []; + $pattern = "/^([\d]+)(ngan|trieu|ty)/i"; + if(preg_match($pattern, $str, $match)){ + return [ + "number" => $match[1], + "unit" => $match[2], + ]; + } + return [ + "number" => 0, + "unit" => 'trieu', + ]; + } + + + // accept url format: + // default: ?min=1000&max=20000 + // custom: ?p=tu-15-20-trieu + protected static function getPriceRange(){ + // default + $min_price = getRequest("min"); + $max_price = getRequest("max"); + + // custom price range query + $price_range = getRequest("p"); // duoi-10-trieu , tu-15-20-trieu, tren-30-trieu + + // duoi-10-trieu + $match = []; + if(preg_match("/^duoi-([0-9]+)-trieu$/i", $price_range, $match)) { + $min_price = ''; + $max_price = $match[1] * 1000000; + } + elseif (preg_match("/^tu-([0-9]+)-([0-9]+)-trieu$/i", $price_range, $match)) { + $min_price = $match[1] * 1000000; + $max_price = $match[2] * 1000000; + } + elseif (preg_match("/^tren-([0-9]+)-trieu$/i", $price_range, $match)) { + $min_price = $match[1] * 1000000; + $max_price = ''; + } + + return [ + "min" => intval($min_price), + "max" => intval($max_price), + ]; + } + + + protected function getOrderingClause($sort_by, &$where_query, $current_viewed_category = 0) { + + // show by global order + $ordering = "ORDER BY ordering DESC, id DESC"; + + switch($sort_by) { + case "order-new"; + $ordering = "ORDER BY `ordering` DESC, `id` DESC"; + break; + case "order-last-update"; + $ordering = "ORDER BY `ordering` DESC, last_update DESC"; + break; + case "last-update"; + $ordering = "ORDER BY last_update DESC"; + break; + case "order"; + $ordering = "ORDER BY `ordering` DESC"; + break; + case "new"; + $ordering = "ORDER BY `id` DESC"; + break; + case "price-asc"; + $where_query .= " AND `price` > 100 "; + $ordering = "ORDER BY `price` ASC"; + break; + case "price-desc"; + $ordering = " ORDER BY `price` DESC "; + break; + case "view"; + $ordering = "ORDER BY `visit` desc"; + break; + } + + + return $ordering; + } +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductHotController.php b/inc/Hura8/Components/Product/AdminController/AProductHotController.php new file mode 100644 index 0000000..7af96fc --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductHotController.php @@ -0,0 +1,40 @@ +objProductHotModel = new ProductHotModel(); + } + + + public function getProductHot(array $product_id_array) { + $hot_script = $this->objProductHotModel->getProductHot($product_id_array); + + //add empty for other products + foreach ($product_id_array as $pro_id) { + if(!isset($hot_script[$pro_id])) $hot_script[$pro_id] = []; + } + + return $hot_script; + } + + + public function updateProductHot($pro_id, array $new_types) { + return $this->objProductHotModel->updateProductHot($pro_id, $new_types); + } + + + +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductSpecGroupAttributeController.php b/inc/Hura8/Components/Product/AdminController/AProductSpecGroupAttributeController.php new file mode 100644 index 0000000..db48431 --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductSpecGroupAttributeController.php @@ -0,0 +1,24 @@ +objProductSpecGroupAttributeModel = new ProductSpecGroupAttributeModel(); + parent::__construct($this->objProductSpecGroupAttributeModel); + } + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + return true; + } +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductSpecGroupController.php b/inc/Hura8/Components/Product/AdminController/AProductSpecGroupController.php new file mode 100644 index 0000000..4b27829 --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductSpecGroupController.php @@ -0,0 +1,74 @@ +objProductSpecGroupModel = new ProductSpecGroupModel(); + parent::__construct($this->objProductSpecGroupModel); + } + + public function getSpecGroupAttributeWithValues($group_id) + { + $objProductAttributeModel = new ProductAttributeModel(); + return $objProductAttributeModel->getSpecGroupAttributeWithValues($group_id); + } + + public function getSpecGroupAttribute($group_id) + { + $objProductAttributeModel = new ProductAttributeModel(); + return $objProductAttributeModel->getSpecGroupAttribute($group_id); + } + + + public function clearProductSpecGroup($product_id) + { + return $this->objProductSpecGroupModel->clearProductSpecGroup($product_id); + } + + + public function setProductSpecGroup($product_id, $group_id) + { + return $this->objProductSpecGroupModel->setProductSpecGroup($product_id, $group_id); + } + + + public function getProductSpec($product_id) + { + return $this->objProductSpecGroupModel->getProductSpec($product_id); + } + + + public function getProductSpecGroupInfo($product_id) + { + return $this->objProductSpecGroupModel->getProductSpecGroupInfo($product_id); + } + + + public function getProductSpecGroupId($product_id) + { + return $this->objProductSpecGroupModel->getProductSpecGroupId($product_id); + } + + + public function getSpecGroupInfo($group_id) + { + return $this->objProductSpecGroupModel->getSpecGroupInfo($group_id); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + return true; + } + +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductVariantController.php b/inc/Hura8/Components/Product/AdminController/AProductVariantController.php new file mode 100644 index 0000000..2d6d132 --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductVariantController.php @@ -0,0 +1,85 @@ +objProductVariantModel = new ProductVariantModel($product_id); + } + + + public function delete($id) { + return $this->objProductVariantModel->delete($id); + } + + + public function updateImage($variant_id, $image_name) { + return $this->objProductVariantModel->updateFields($variant_id, [ + "thumbnail" => $image_name + ]); + } + + public function removeImage($variant_id) { + return $this->objProductVariantModel->updateFields($variant_id, [ + "thumbnail" => '' + ]); + } + + + public function getInfo($id) { + return $this->objProductVariantModel->getInfo($id); + } + + public function getProductVariantOption($product_id){ + return $this->objProductVariantModel->getProductVariantOption($product_id); + } + + + //update product's variant options + public function updateVariantOption($attribute) { + return $this->objProductVariantModel->updateVariantOption($attribute); + } + + public function useVariantOptionSample($select_id) { + $sample = $this->objProductVariantModel->useVariantOptionSample($select_id); + + return ($sample) ? \json_decode($sample,true) : []; + } + + + //use a product's variant-option to create a choice, so next product can select without recreate from beginning + public function createVariantOptionSample($use_from_pro_id, $sample_title) { + return $this->objProductVariantModel->createVariantOptionSample($use_from_pro_id, $sample_title); + } + + + public function getVariantOptionSample() { + return $this->objProductVariantModel->getVariantOptionSample(); + } + + public function updateVariant($variant_id, array $variant_info) { + if($variant_id) { + return $this->objProductVariantModel->update($variant_id, $variant_info); + }else{ + return $this->objProductVariantModel->create($variant_info); + } + } + + public function getProductVariantPriceRange(){ + return $this->objProductVariantModel->getProductVariantPriceRange(); + } + + public function getProductVariantList(){ + return $this->objProductVariantModel->getList([]); + } + + +} diff --git a/inc/Hura8/Components/Product/Controller/ProductFilterBuilderController.php b/inc/Hura8/Components/Product/Controller/ProductFilterBuilderController.php new file mode 100644 index 0000000..b6ff89c --- /dev/null +++ b/inc/Hura8/Components/Product/Controller/ProductFilterBuilderController.php @@ -0,0 +1,320 @@ + '', + 'host' => '', + 'port' => '', + 'user' => '', + 'pass' => '', + 'path' => '', + 'query' => [], + 'fragment' => '', + ]; + + protected $_price_format = ''; + protected $_brand_format = ''; + protected $_filter_format = ''; + + + public function __construct($request_url) { + + if(defined('PRICE_FILTER_FORMAT') && PRICE_FILTER_FORMAT) { + $this->_price_format = PRICE_FILTER_FORMAT; + } + + if(defined('BRAND_FILTER_FORMAT') && BRAND_FILTER_FORMAT) { + $this->_brand_format = BRAND_FILTER_FORMAT; + } + + if(defined('ATTRIBUTE_FILTER_FORMAT') && ATTRIBUTE_FILTER_FORMAT ) { + $this->_filter_format = ATTRIBUTE_FILTER_FORMAT; + } + + $this->_request_url = $request_url; + $this->_url_elements = Url::parse($request_url); + } + + + public function buildProductFilterFromUrl(array $existing_filters = []) { + $filters = [ + "price" => $this->getPriceFilterRange(), + "brand" => $this->getBrandFilter(), // array(1,2,3,) + "collection" => $this->getGenericFilter('collection'), // array(1,2,3,) + "supplier" => $this->getGenericFilter('supplier'), // array(1,2,3,) + "rating" => $this->getGenericFilter('rating'), // array(1,2,3,) + "category" => $this->getGenericFilter('category'), // array(1,2,3,) + "status" => array(), // array(1,2,3,) + "query" => (isset($this->_url_elements['query']['q'])) ? $this->_url_elements['query']['q'] : '', // string search keyword + "hotType" => $this->getGenericFilter('hotType', 'string'),// array(saleoff | not | new) + "attribute" => $this->getAttributeFilter(), // array(1,2,3,) + "ids" => $this->getGenericFilter('ids'), // array(1,2,3,) + "promotion" => "", + "storeId" => $this->getGenericFilter('store'), // array(1,2,3,) + "other_filter" => $this->getGenericFilter('other_filter', 'string'), // array(in-stock, has-promotion etc...) + "spec_group_id" => $this->getGenericFilter('spec_group_id'), + ]; + + + // debug_var(array_merge($filters, $existing_filters)); + if(sizeof($existing_filters)) { + foreach ($existing_filters as $key => $values) { + if(!isset($filters[$key])) continue; + if(!$values) continue; + + if(is_array($values) && is_array($filters[$key])) { + $filters[$key] = array_merge($filters[$key], $values); + }else{ + $filters[$key] = $values; + } + } + } + + return $filters ; + } + + + protected function getGenericFilter($url_para, $data_type='int') { + $value = (isset($this->_url_elements['query'][$url_para])) ? $this->_url_elements['query'][$url_para] : ''; + + if($data_type == 'int') { + $safe_value = trim(preg_replace("/[^0-9_\-\,]/i", '', $value)); + }else{ + $safe_value = trim(preg_replace("/[^a-z0-9_\-\.\,]/i", '', $value)); + } + + if(!$safe_value) return []; + + $safe_value_list = explode(FILTER_VALUE_SEPARATOR, $safe_value); + + return array_values(array_filter($safe_value_list)); + } + + + protected function getBrandFilter() + { + if($this->_brand_format == 'brand_index'){ + + $objBrandModel = new BrandModel(); + + $result = []; + $brand_filters = $this->getGenericFilter('brand', 'string'); + + foreach ($brand_filters as $_brand) { + if(is_int($_brand)) { + $result[] = $_brand; + } + + $brand_info = $objBrandModel->getInfoByUrl( $_brand); + if(!$brand_info) continue; + + $result[] = $brand_info['id']; + } + + return $result; + } + + + // default + return $this->getGenericFilter('brand'); + } + + + protected function getAttributeFilter(array $category_list_id = []) { + + if($this->_filter_format == 'filter_code') { + + $excluded_keys = [ + 'q', + 'p', + 'min', 'max', + 'brand', + 'page', + 'request_path', + 'category', + 'supplier', 'collection', + 'ids', + 'filter',' hotType', + 'other_filter', + 'rating', 'price', + // special ajax key + 'action','action_type', '_auto' + ]; + $attr_filter_params = []; + + foreach ( $this->_url_elements['query'] as $key => $value) { + if(in_array($key, $excluded_keys) || !$value) continue; + + $attr_filter_params[$key] = explode(',', urldecode(urldecode($value))); + } + + if(sizeof($attr_filter_params)) { + + $category_attributes = $this->getAttributesForCategory($category_list_id); + //debug_var($category_attributes); + + $match_attr_value_ids = []; + foreach ($attr_filter_params as $attr_filter_code => $attr_value_api_key_list) { + + foreach ($category_attributes as $attribute) { + if($attribute['filter_code'] != $attr_filter_code) continue; + + foreach ($attribute['value_list'] as $_index => $_value_info) { + + foreach ($attr_value_api_key_list as $attr_value_api_key) { + if($_value_info['api_key'] == $attr_value_api_key) { + $match_attr_value_ids[] = $_value_info['id']; + } + } + } + } + } + + + return $match_attr_value_ids; + } + } + + // default + return $this->getGenericFilter('filter'); + } + + + + protected function getAttributesForCategory(array $category_ids){ + + $category_att_id_list = array(); + $attr_query_cond = (sizeof($category_ids)) ? " AND category_id IN (".join(',', $category_ids).") " : ""; + + $query = $this->db->runQuery(" SELECT `attr_id` FROM `idv_attribute_category` WHERE 1 ". $attr_query_cond); + foreach ($this->db->fetchAll($query) as $rs ){ + $category_att_id_list[] = $rs['attr_id']; + } + + if(!sizeof($category_att_id_list)) return array(); + + $query = $this->db->query(" + SELECT + attval.id , + attval.attributeId , + attval.value , + attval.api_key , + att.attribute_code , + att.filter_code + FROM ".TB_ATTRIBUTE_VALUE." attval + LEFT JOIN ".TB_ATTRIBUTE." att ON attval.attributeId = att.id + WHERE att.id IN (".join(',', $category_att_id_list).") AND att.isSearch=1 + ORDER BY attval.ordering DESC "); + + $attribute_list = []; + + foreach ($this->db->fetchAll($query) as $rs ){ + $att_value_id = $rs['id']; + $att_id = $rs['attributeId']; + + $value_info = array( + "id" => $att_value_id, + "api_key" => $rs['api_key'], + ); + + if(!array_key_exists($att_id, $attribute_list)) { + $attribute_list[$att_id] = [ + "id" => $att_id, + "code" => $rs['attribute_code'], + "filter_code" => $rs['filter_code'], + 'value_list' => [$value_info], + ]; + }else{ + $attribute_list[$att_id]['value_list'][] = $value_info; + } + } + + + return array_values($attribute_list); + } + + + // accept url format: + // default: ?min=1000&max=20000 + // custom: ?p=15trieu-20-trieu + protected function getPriceFilterRange(){ + // default format + if($this->_price_format != 'p') { + return array( + "min" => isset($this->_url_elements['query']['min']) ? intval($this->_url_elements['query']['min']) : 0 , + "max" => isset($this->_url_elements['query']['max']) ? intval($this->_url_elements['query']['max']) : 0 , + ); + } + + // custom price range query + $price_range_format = (isset($this->_url_elements['query']['p'])) ? $this->_url_elements['query']['p'] : ''; // duoi-10trieu , 10ngan-2trieu, 3trieu-6trieu, tren-30trieu + + if(strpos($price_range_format, '-') === false) { + return array( + "min" => 0, + "max" => 0, + ); + } + + $unit_translation = [ + 'ngan' => 1000, + 'trieu' => 1000000, + 'ty' => 1000000000, + ]; + + // duoi-10trieu, duoi-10ngan + if(strpos($price_range_format, 'duoi-') !== false) { + $unit_match = $this->findPriceUnitMatch(str_replace("duoi-", "", $price_range_format)); + return array( + "min" => 0, + "max" => $unit_match['number'] * $unit_translation[$unit_match['unit']], + ); + } + + // tren-10trieu, tren-10ngan + if(strpos($price_range_format, 'tren-') !== false) { + $unit_match = $this->findPriceUnitMatch(str_replace("tren-", "", $price_range_format)); + return array( + "min" => $unit_match['number'] * $unit_translation[$unit_match['unit']], + "max" => 0, + ); + } + + // 10ngan-2trieu, 3trieu-6trieu + $parts = explode('-', $price_range_format); + $min_part = $parts[0]; + $max_part = $parts[1]; + $min_match = $this->findPriceUnitMatch($min_part); + $max_match = $this->findPriceUnitMatch($max_part); + + return [ + "min" => $min_match['number'] * $unit_translation[$min_match['unit']], + "max" => $max_match['number'] * $unit_translation[$max_match['unit']], + ]; + } + + + protected function findPriceUnitMatch($str){ + $match = []; + $pattern = "/^([\d]+)(ngan|trieu|ty)/i"; + if(preg_match($pattern, $str, $match)){ + return [ + "number" => $match[1], + "unit" => $match[2], + ]; + } + return [ + "number" => 0, + "unit" => 'trieu', + ]; + } +} diff --git a/inc/Hura8/Components/Product/Controller/ProductFilterController.php b/inc/Hura8/Components/Product/Controller/ProductFilterController.php new file mode 100644 index 0000000..979ca41 --- /dev/null +++ b/inc/Hura8/Components/Product/Controller/ProductFilterController.php @@ -0,0 +1,797 @@ + translated to brand=>array(2,), + - ?monitor_size=12inch&color=red => translated to attribute=> array(123, 23121,) + */ + protected $filters = [ + "price" => array('max' => 0, 'min'=> 0), + "brand" => array(), // array(1,2,3,) + "collection" => array(), // array(1,2,3,) + "supplier" => array(), // array(1,2,3,) + "rating" => array(), // array(1,2,3,) + "category" => array(), // array(1,2,3,) + "status" => array(), // array(1,2,3,) + "query" => "", // string search keyword + "hotType" => array(),// array(saleoff | not | new) + "attribute" => array(), // array(1,2,3,) + "ids" => array(), // array(1,2,3,) + "excluded_ids" => array(), // array(1,2,3,) + "promotion" => "", + "storeId" => array(), // array(1,2,3,) + "other_filter" => array(), // array(in-stock, has-promotion etc...) + "spec_group_id" => array(), // array(1,2,3,) + ]; + + + public function __construct($request_url = '') { + $this->_request_url = $request_url; + } + + + public function getResult( + $current_page_id =1, + $number_per_page=10, + $sort_by = 'new', + array $existing_filters=[], + $current_category_ids = [], + $price_range = [] + ) { + + $objProductFilterBuilder = new ProductFilterBuilderController($this->_request_url); + $built_filters = $objProductFilterBuilder->buildProductFilterFromUrl($existing_filters); + $this->setFilters($built_filters); + + $product_result = $this->findProductListIdMatchFilters($current_page_id, $number_per_page, $sort_by); + //debug_var($product_result); + + $objProductFilterOptions = new ProductFilterOptionsController( + $this->getFilters(), + $current_category_ids, + $product_result['item_list'], + $price_range + ); + $objProductFilterOptionsTranslation = new ProductFilterOptionsTranslationController($this->_request_url); + + list ( $filter_options, $filter_messages ) = $objProductFilterOptionsTranslation->getAllFilterOptions( + $objProductFilterOptions->categoryList(), + $objProductFilterOptions->attributeList(), + $objProductFilterOptions->brandList(), + $objProductFilterOptions->priceList() + ); + + return [ + 'total' => $product_result['total'], + 'paged_product_list' => $product_result['item_list'], + 'filter_options' => $filter_options, + 'filter_messages' => $filter_messages, + ]; + } + + + public function getFilters() { + return $this->filters; + } + + + // allow to update filters + public function setFilters(array $new_filters) { + foreach ($new_filters as $key => $value) { + if(isset($this->filters[$key])) { + $this->filters[$key] = $value; + } + } + } + + + protected function createFilterCacheKey($sort_by) { + return md5(\json_encode($this->filters) . $sort_by); + } + + + // find a list of products that match filtering conditions + protected function findProductListIdMatchFilters($current_page_id, $number_per_page, $sort_by = "new" ) { + + $cache_key = $this->createFilterCacheKey($sort_by); + + return self::getCache($cache_key, function () use ($current_page_id, $number_per_page, $sort_by) { + return $this->findProductListIdMatchFilters_raw($current_page_id, $number_per_page, $sort_by ); + }); + } + + + protected function findProductListIdMatchFilters_raw($current_page_id=1, $number_per_page=10, $sort_by = "new" ) { + + list( , , , $where_query ) = $this->buildSQLConditionFromFilters($sort_by); + + $ordering_clause = $this->getOrderingClause($sort_by); + + $objProductFilterModel = new ProductFilterModel(); + + list($total_number, $item_list) = $objProductFilterModel->findProductListIdMatchFilters([ + $where_query, + $current_page_id, + $number_per_page, + $ordering_clause + ]); + + + $final_result = [ + "total" => $total_number, + "item_list" => $item_list, + ]; + + return $final_result; + } + + + protected function buildSQLConditionFromFilters( $sort_by = "new" ) { + //list of available filters + $filterPath = []; + $filter_messages = []; + $where_query = []; + + // system controls + if( ENABLE_PRODUCT_EXPIRE ) { + $where_query[] = " AND (from_time =0 OR from_time < '".CURRENT_TIME."') + AND (to_time > '".CURRENT_TIME."' OR to_time=0 ) "; + } + + //other filters + if( array_key_exists("other_filter", $this->filters) && sizeof($this->filters["other_filter"])) { + foreach ($this->filters["other_filter"] as $_filter) { + switch ($_filter) { + case "in-stock"; + $filter_messages[] = [ + 'title' => 'Còn hàng', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND quantity > 0 "; + break; + case "has-vat"; + $filter_messages[] = [ + 'title' => 'Có thuế VAT', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND `has_vat` = 1 "; + break; + case "out-stock"; + $filter_messages[] = [ + 'title' => 'Hết hàng', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND `quantity` = 0 "; + break; + case "has-market-price"; + $filter_messages[] = [ + 'title' => 'Có giá thị trường', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND market_price > 0 "; + break; + case "no-price"; + $filter_messages[] = [ + 'title' => 'Không có giá', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND `price` = 0 "; + break; + case "no-warranty"; + $filter_messages[] = [ + 'title' => 'Không có bảo hành', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND LENGTH(warranty) < 2 "; + break; + case "no-sku"; + $filter_messages[] = [ + 'title' => 'Không mã kho hàng', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND LENGTH(sku) < 2 "; + break; + case "has-config"; + $filter_messages[] = [ + 'title' => 'Không có cấu hình', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND `config_count` > 0 "; + break; + case "no-image"; + $filter_messages[] = [ + 'title' => 'Không có ảnh', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND image_count = 0 "; + break; + case "no-category"; + $filter_messages[] = [ + 'title' => 'Không có danh mục', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND `category` = '0' "; + break; + case "display-off"; + $filter_messages[] = [ + 'title' => 'Đang ẩn', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND `status`=0 "; + break; + case "display-on"; + $filter_messages[] = [ + 'title' => 'Đang hiển thị', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND `status`=1 "; + break; + case "has-promotion": + $filter_messages[] = [ + 'title' => 'Có khuyến mại', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND LENGTH(`special_offer`) < 5 "; + break; + //...add more + } + } + } + + /*// limit by admin permission + //1-1-2014 gioi han theo san pham danh muc ma quan tri nay quan ly + global $admin_permission; + $restricted_category_per_admin = (isset($admin_permission['permit'])) ? $admin_permission['permit']['product:category']['ids'] : []; + if(sizeof($restricted_category_per_admin)) { + $cat_query = $this->db->runQuery(" + SELECT DISTINCT pro_id + FROM ".TB_PRODUCT_CATEGORY." + WHERE `category_id` IN (".join(",", $restricted_category_per_admin).") + LIMIT 5000 + "); + + //lay danh sach danh muc cua san pham + $product_in_category = []; + foreach( $this->db->fetchAll($cat_query) as $cat_info ){ + $product_in_category[] = $cat_info["pro_id"]; + } + + $where_query[] = (sizeof($product_in_category)) ? " AND id IN (". join(',', $product_in_category) .") " : " AND id = 0 "; + }*/ + + + //- brand id or ids or brand_indexes + if( array_key_exists("brand", $this->filters) && sizeof($this->filters["brand"])) { + + $objBrandModel = new BrandModel(); + + $condition = array(); + foreach ($this->filters["brand"] as $brand_id) { + if(!$brand_id) continue; + + $brand_info = $objBrandModel->getInfo( $brand_id); + if(!$brand_info) continue; + + $filterPath["brand"][] = array( + "id" => $brand_info['id'], + "name" => $brand_info["title"], + ); + + $condition[] = " brandId = '".intval($brand_info['id'])."' "; + $filter_messages[] = [ + 'title' => $brand_info['title'], + 'reset' => Url::buildUrl($this->_request_url, ['brand' => join(FILTER_VALUE_SEPARATOR, remove_item_from_array($this->filters["brand"], $brand_id ))] ), + ]; + } + + if(sizeof($condition)) { + $where_query[] = " AND ( ".join(" OR ", $condition)." )"; + } + } + + + //- collection id or ids + if( array_key_exists("collection", $this->filters) && sizeof($this->filters["collection"])) { + $condition = array(); + + $objProductCollectionModel = new ProductCollectionModel(); + + foreach ($this->filters["collection"] as $_id) { + + $collection_info = $objProductCollectionModel->getInfo($_id); + if(!$collection_info) continue; + + $filterPath["collection"][] = array( + "id" => $_id, + "name" => $collection_info["title"], + ); + $condition[] = " `id` IN ( SELECT product_id FROM ".TableName::PRODUCT_PER_COLLECTION." WHERE `collection_id`='".intval($collection_info['id'])."' ) "; + $filter_messages[] = [ + 'title' => 'Bộ sưu tập ', + 'reset' => Url::buildUrl($this->_request_url, ['collection' => join(',', remove_item_from_array($this->filters["collection"], $_id))] ), + ]; + } + + $where_query[] = " AND ( ".join(" OR ", $condition)." )"; + } + + //- supplier id + if( array_key_exists("supplier", $this->filters) && sizeof($this->filters["supplier"])) { + /*$condition = array(); + + if(!$this->objSupplier) $this->objSupplier = new Supplier(); + + foreach ($this->filters["supplier"] as $_id) { + $_id = intval($_id); + + $supplier_info = $this->objSupplier->getInfo($_id); + + $filterPath["supplier"][] = array( + "id" => $_id, + "name" => $supplier_info["name"], + ); + $condition[] = " supplier_id = '".$_id."' "; + $filter_messages[] = [ + 'title' => $supplier_info["name"], + 'reset' => Url::buildUrl($this->_request_url, ['supplier' => join(',', remove_item_from_array($this->filters["supplier"], $_id))] ), + ]; + } + + $where_query[] = " AND ( ".join(" OR ", $condition)." )";*/ + } + + //- rating: 1-> 5 + if( array_key_exists("rating", $this->filters) && sizeof($this->filters["rating"])) { + $condition = array(); + foreach ($this->filters["rating"] as $_id) { + + $condition[] = " `rating` = '".intval($_id)."' "; + $filterPath["rating"][] = array( + "id" => $_id, + "name" => $_id, + ); + $filter_messages[] = [ + 'title' => 'Đánh giá '.$_id, + 'reset' => Url::buildUrl($this->_request_url, ['rating' => join(',', remove_item_from_array($this->filters["rating"], $_id))] ), + ]; + } + + $where_query[] = " AND ( ".join(" OR ", $condition)." )"; + } + + //- category id or ids + if( array_key_exists("category", $this->filters) && sizeof($this->filters["category"])) { + + $objProductCategoryModel = new ProductCategoryModel(); + + $condition = array(); + foreach ($this->filters["category"] as $cat_id) { + + $cat_info = $objProductCategoryModel->getInfo($cat_id); + if(!$cat_info) continue; + + if($cat_info["is_parent"]) { + $childListId = ($cat_info["child_ids"]) ?: '0'; + $condition[] = " `category_id` IN (".$childListId .") "; + }else{ + $condition[] = " `category_id` = '".$cat_id."' "; + } + + $filterPath["category"][] = array( + "id" => $cat_id, + "name" => $cat_info['title'], + ); + + $filter_messages[] = [ + 'title' => $cat_info['title'], + 'reset' => Url::buildUrl($this->_request_url, ['category' => join(FILTER_VALUE_SEPARATOR, remove_item_from_array($this->filters["category"], $cat_id))] ), + ]; + } + + if(sizeof($condition)) { + $where_query[] = " AND `id` IN ( SELECT DISTINCT `item_id` FROM ".TableName::PRODUCT_PER_CATEGORY." WHERE " . join(" OR ", $condition) . " )"; + } + } + + //- status: 0|1|null + if( array_key_exists("status", $this->filters) && sizeof($this->filters["status"])) { + $where_query[] = " AND `status` IN (".join(',', $this->filters["status"]).") "; + } + + // spec_group_id + if( array_key_exists("spec_group_id", $this->filters) && sizeof($this->filters["spec_group_id"])) { + $where_query[] = " AND `spec_group_id` IN (".join(',', $this->filters["spec_group_id"]).") "; + } + + // detail_page_only: 0 | 1 + if( array_key_exists("detail_page_only", $this->filters) && $this->filters["detail_page_only"]) { + $where_query[] = " AND `detail_page_only` = '".intval($this->filters['detail_page_only'])."' "; + } + + + //- query: search keyword + if( array_key_exists("query", $this->filters) && $this->filters["query"]) { + $keyword_search = $this->filters["query"]; + $search_by_product_id = intval(preg_replace('/[^0-9]/i', '', $keyword_search)); + + // todo: add more + $filter_search = []; + if(isset($this->filters["brand"]) && $this->filters["brand"]) { + $filter_search['brand'] = ["IN", $this->filters["brand"]]; + } + + if(isset($this->filters["status"]) && $this->filters["status"]) { + $filter_search['status'] = ["IN", $this->filters["status"]]; + } + + $objProductSearchModel = new ProductSearchModel(); + $match_result = $objProductSearchModel->find($keyword_search, $filter_search); + + //debug_var($match_result); + + $filterPath["search"] = array( + "id" => $keyword_search, + "name" => $keyword_search, + ); + $filter_messages[] = [ + 'title' => $keyword_search, + 'reset' => Url::buildUrl($this->_request_url, ['q' => ''] ), + ]; + + if($search_by_product_id > 0) { + $where_query[] = (sizeof($match_result) > 0) ? " AND ( `id` IN (".join(',', $match_result).") OR `id` = '".$search_by_product_id."' ) " : " AND `id` = '".$search_by_product_id."' "; + }else{ + $where_query[] = (sizeof($match_result) > 0) ? " AND `id` IN (".join(',', $match_result).") " : " AND `id` = -1 "; + } + + } + + + //- hotType: saleoff | not | new | or combination of types + if( array_key_exists("hotType", $this->filters) && sizeof($this->filters["hotType"])) { + $config_hottype = AProductHotController::getProductHotTypeList(); + $condition = array(); + foreach ($this->filters["hotType"] as $_id) { + if(!array_key_exists($_id, $config_hottype)) continue; + + $condition[] = " `hot_type` = '".$_id."' "; + + $filter_messages[] = [ + 'title' => $config_hottype[$_id], + 'reset' => Url::buildUrl($this->_request_url, ['hotType' => '']), + ]; + } + + if(sizeof($condition)) { + $where_query[] = " AND `id` IN (SELECT pro_id FROM ".TB_PRODUCT_HOT." WHERE 1 AND ( ".join(" OR ", $condition)." ) ) "; + } + + } + + + //- attribute values + /* + if( array_key_exists("attribute", $this->filters) && sizeof($this->filters["attribute"])) { + $filter_attr_value_list = $this->filters["attribute"]; + //filter = attr_value_1-attr_value_2-attr_value_3, + $query_attr_id = []; + $count_filter = 0; + + foreach($filter_attr_value_list as $attr_id){ + $attr_id = (int) $attr_id; + if($attr_id) { + $query_attr_id[] = $attr_id; + $count_filter ++; + } + } + + $product_filter_id_match = array(); + if(sizeof($query_attr_id)) { + $query = $this->db->runQuery(" + SELECT DISTINCT pro_id , COUNT(*) as num_pro + FROM ".TB_PRODUCT_ATTRIBUTE." + WHERE attr_value_id IN (".join(',', $query_attr_id).") + GROUP BY pro_id + HAVING num_pro = ".$count_filter." + LIMIT 10000 + "); + foreach ( $this->db->fetchAll($query) as $rs ) { + $product_filter_id_match[] = $rs["pro_id"]; + } + } + + $where_query[] = (sizeof($product_filter_id_match)) ? " AND `id` IN (".join(', ', $product_filter_id_match).") " : " AND `id` = 0 " ; + + //xay lai url de back + if(!$this->objCategoryProduct) $this->objCategoryProduct = new CategoryProduct(); + + foreach($filter_attr_value_list as $value_id ){ + $att_name = $this->objCategoryProduct->atrValueName($value_id); + + $filterPath["attribute"][] = array( + "id" => $value_id, + "name" => $att_name, + ); + $filter_messages[] = [ + 'title' => $att_name, + 'reset' => Url::buildUrl($this->_request_url, ['filter' => join(FILTER_VALUE_SEPARATOR, remove_item_from_array($this->filters["attribute"], $value_id))] ), + ]; + } + + } + + */ + + //- attribute values + if( array_key_exists("attribute", $this->filters) && sizeof($this->filters["attribute"])) { + + $filter_attr_value_list = $this->filters["attribute"]; + + $attr_values_per_attribute = $this->groupAttributeValuesByAttributeId($filter_attr_value_list); + + $having_match_count = sizeof(array_keys($attr_values_per_attribute)); + + $attribute_info_list = $this->groupAttributeListInfo(array_keys($attr_values_per_attribute)); + + $attr_where_query = []; + foreach ($attr_values_per_attribute as $_att_id => $_att_value_list) { + // if same attribute : find products that match either ONE of these values (not match ALL values) + $_att_info = $attribute_info_list[$_att_id]; + if (!$_att_info) continue; + + // this attribute requires all products must have ALL selected values + if ($_att_info['value_match_all'] && sizeof($_att_value_list) > 1) { + + $_product_id_match_all = $this->getProductMatchAllAttributeValueIds($_att_value_list); + + if (sizeof($_product_id_match_all) > 0) { + $attr_where_query[] = " ( `id` IN (" . join(",", $_product_id_match_all) . ") ) "; + } else { + $attr_where_query[] = " ( `id` = -1 ) "; + } + + } else { + $attr_where_query[] = " ( SELECT DISTINCT `pro_id` FROM " . TB_PRODUCT_ATTRIBUTE . " WHERE (" . + join(" OR ", array_map(function ($_item) { return " `attr_value_id` = '" . intval($_item['id']) . "' "; }, $_att_value_list)) + . " ) ) "; + } + + } + + $product_filter_id_match = array(); + + if (sizeof($attr_where_query)) { + $query = $this->db->runQuery(" + SELECT `pro_id` , COUNT(*) AS num_pro + FROM ( " . join(" UNION ALL ", $attr_where_query) . " ) AS tpm_tb + GROUP BY pro_id + HAVING num_pro = " . $having_match_count . " + LIMIT 10000 + "); + foreach ($this->db->fetchAll($query) as $rs) { + $product_filter_id_match[] = $rs["pro_id"]; + } + } + + $where_query[] = (sizeof($product_filter_id_match)) ? " AND " . TB_PRODUCT_LIGHT . ".`id` IN (" . join(', ', $product_filter_id_match) . ") " : " AND " . TB_PRODUCT_LIGHT . ".`id` = 0 "; + + //xay lai url de back + $objProductAttributeModel = new ProductAttributeModel(); + + foreach($filter_attr_value_list as $value_id ){ + $att_name = $objProductAttributeModel->getInfo($value_id)['title']; + + $filterPath["attribute"][] = array( + "id" => $value_id, + "name" => $att_name, + ); + $filter_messages[] = [ + 'title' => $att_name, + 'reset' => Url::buildUrl($this->_request_url, ['filter' => join(FILTER_VALUE_SEPARATOR, remove_item_from_array($this->filters["attribute"], $value_id))] ), + ]; + } + + /*//xay lai url de back + $filterPath["attribute"] = $filter_attr_value_list; + + foreach ($attr_values_per_attribute as $_att_id => $_att_value_list) { + foreach ($_att_value_list as $_item) { + $filter_messages[] = [ + 'title' => $_item['title'], + 'reset' => Url::buildUrl(CURRENT_URL, ['filter' => join('-', remove_item_from_array($filter_attr_value_list, $_item['id']))]), + ]; + } + }*/ + + } + + + //given products' ids + if( array_key_exists("ids", $this->filters) && sizeof($this->filters["ids"])) { + + $filter_messages[] = [ + 'title' => "IDs", + 'reset' => Url::buildUrl($this->_request_url, ['ids' => '']), + ]; + + $where_query[] = " AND `id` IN (". join(",", $this->filters["ids"]) .") "; + } + + // exclude some ids + if( array_key_exists("excluded_ids", $this->filters) && sizeof($this->filters["excluded_ids"])) { + + $filter_messages[] = [ + 'title' => "Excluded IDs", + 'reset' => Url::buildUrl($this->_request_url, ['excluded_ids' => '']), + ]; + + $where_query[] = " AND `id` NOT IN (". join(",", $this->filters["excluded_ids"]) .") "; + } + + $price_range_query_limit = join(" ",$where_query); + + //- price range + if(isset($this->filters["price"]) && sizeof($this->filters["price"]) && ($this->filters["price"]['min'] > 0 || $this->filters["price"]['max'] > 0)) { + //limit by price range + $maxPrice = clean_price($this->filters["price"]['max']); + $minPrice = clean_price($this->filters["price"]['min']); + + $price_range_query = ''; + if($maxPrice > 0 && $minPrice > 0){ + $price_range_query = " ( `price` BETWEEN '".$minPrice."' AND '".$maxPrice."' ) "; + + }else if($maxPrice > 0){ + $price_range_query = " `price` < '".$maxPrice."' "; + + }else if($minPrice > 0){ + $price_range_query = " `price` >='".$minPrice."' "; + } + + /*if(ENABLE_INVENTORY_PER_STORE && USER_LOCATION) { + + $list_ids = $this->getProductIds($price_range_query_limit); + $this->objCache->set("all_product_ids_location", $list_ids); + + $objUStoreLocation = new UStoreLocation(); + $location_products = $objUStoreLocation->getAllProductForLocation(USER_LOCATION, $list_ids, $sort_by); + $location_products = $this->filterLocationProductByPrice($location_products, $maxPrice, $minPrice); + + $this->objCache->set("location_products", $location_products); + + $location_product_ids = array_keys($location_products); + $where_query[] = (sizeof($location_product_ids)) ? " AND `id` IN (".join(',', $location_product_ids).") " : " AND `id` = 0"; + + } else { + $where_query[] = " AND ". $price_range_query; + }*/ + + $where_query[] = " AND ". $price_range_query; + + $filterPath["price"] = array( + "min" => $minPrice, + "max" => $maxPrice, + ); + + $price_format = ProductFilterPrice::buildPriceRangeFormat($minPrice, $maxPrice); + + $filter_messages[] = [ + 'title' => $price_format['title'], + 'reset' => Url::buildUrl($this->_request_url, ['p' => '', 'min' => '', 'max' => '']) + ]; + } + + return [ + $price_range_query_limit, + $filterPath, + $filter_messages, + $where_query + ]; + } + + + protected function getProductMatchAllAttributeValueIds(array $_att_value_list) { + $objProductAttributeModel = new ProductAttributeModel(); + return $objProductAttributeModel->getProductMatchAllAttributeValueIds($_att_value_list); + } + + + protected function groupAttributeListInfo(array $attr_ids) { + + $objProductAttributeModel = new ProductAttributeModel(); + $attr_list_info = $objProductAttributeModel->getListByIds($attr_ids); + + $final_list = []; + foreach ($attr_ids as $_id) { + $final_list[$_id] = (isset($attr_list_info[$_id])) ? $attr_list_info[$_id] : null; + } + + return $final_list; + } + + + protected function groupAttributeValuesByAttributeId(array $attr_value_ids) { + + $objProductAttributeValueModel = new ProductAttributeValueModel(0); + + $result = array(); + foreach ( $objProductAttributeValueModel->getListByIds($attr_value_ids) as $info ) { + $result[$info['attributeId']][] = [ + 'id' => $info['id'], + 'title' => $info['value'], + ]; + } + + return $result; + } + + + protected function filterLocationProductByPrice(array $product_list, $max_price, $min_price) { + if($max_price > 0 && $min_price > 0){ + return array_filter($product_list, function ($pro) use ($max_price, $min_price) { + return ($pro['price'] >= $min_price && $pro['price'] < $max_price); + }); + } + + if($max_price > 0){ + return array_filter($product_list, function ($pro) use ($max_price, $min_price) { + return ($pro['price'] < $max_price); + }); + } + + if($min_price > 0){ + return array_filter($product_list, function ($pro) use ($max_price, $min_price) { + return ($pro['price'] >= $min_price); + }); + } + + return $product_list; + } + + + protected function getOrderingClause($sort_by) { + $mappings = [ + 'order-new' => "ORDER BY `ordering` DESC, `id` DESC", + 'order-last-update' => "ORDER BY `ordering` DESC, last_update DESC", + 'last-update' => "ORDER BY `last_update` DESC", + 'order' => "ORDER BY `ordering` DESC", + 'new' => "ORDER BY `id` DESC", + 'price-asc' => "ORDER BY `price` asc", + 'price-desc' => "ORDER BY `price` DESC ", + 'view' => "ORDER BY `visit` desc", + 'ranking' => "ORDER BY `ranking` DESC", + 'name' => "ORDER BY `title` asc", + ]; + + if(array_key_exists($sort_by, $mappings)) { + return $mappings[$sort_by]; + } + + return "ORDER BY `ordering` DESC, `id` DESC"; + } +} diff --git a/inc/Hura8/Components/Product/Controller/ProductFilterOptionsController.php b/inc/Hura8/Components/Product/Controller/ProductFilterOptionsController.php new file mode 100644 index 0000000..c293a94 --- /dev/null +++ b/inc/Hura8/Components/Product/Controller/ProductFilterOptionsController.php @@ -0,0 +1,104 @@ + array('max' => 0, 'min'=> 0), + "brand" => array(), // array(1,2,3,) + "collection" => array(), // array(1,2,3,) + "supplier" => array(), // array(1,2,3,) + "rating" => array(), // array(1,2,3,) + "category" => array(), // array(1,2,3,) + "status" => array(), // array(1,2,3,) + "query" => "", // string search keyword + "hotType" => array(),// array(saleoff | not | new) + "attribute" => array(), // array(1,2,3,) + "ids" => array(), // array(1,2,3,) + "promotion" => "", + "storeId" => array(), // array(1,2,3,) + "other_filter" => array() // array(in-stock, has-promotion etc...) + ]; + + protected $objProductFilterModel; + + public function __construct($filters, $current_category_ids = [], $list_product_ids = [], $price_range = []) { + + $this->objProductFilterModel = new ProductFilterModel(); + + $this->_filters = $filters; + $this->_current_category_ids = $current_category_ids; + $this->_list_product_ids = $list_product_ids; + $this->_price_range = $price_range; + } + + + public function ratingList() { + + $neededList = array(); + + foreach ( $this->objProductFilterModel->ratingList($this->_list_product_ids) as $result ) { + $neededList[] = array( + "rating" => $result['review_rate'] , + "count" => $result['num'] , + 'is_selected' => in_array($result['review_rate'], $this->_filters['rating']), + ); + } + + return $neededList; + } + + + public function categoryList() { + + if(!sizeof($this->_list_product_ids)) { + return []; + } + + return $this->objProductFilterModel->categoryList($this->_filters['category'], $this->_list_product_ids); + } + + + public function brandList() { + + if(!sizeof($this->_list_product_ids)) { + return []; + } + + return $this->objProductFilterModel->brandList($this->_filters['brand'], $this->_list_product_ids); + } + + + public function attributeList() { + + return $this->objProductFilterModel->attributeList( + $this->_filters['attribute'], + $this->_current_category_ids, + $this->_list_product_ids + ); + } + + + public function priceList() { + + return $this->objProductFilterModel->priceList( + $this->_filters['price']['max'] , + $this->_filters['price']['min'], + $this->_list_product_ids, + $this->_price_range + ); + + } + + +} diff --git a/inc/Hura8/Components/Product/Controller/ProductFilterOptionsTranslationController.php b/inc/Hura8/Components/Product/Controller/ProductFilterOptionsTranslationController.php new file mode 100644 index 0000000..a0926db --- /dev/null +++ b/inc/Hura8/Components/Product/Controller/ProductFilterOptionsTranslationController.php @@ -0,0 +1,348 @@ + '', + 'host' => '', + 'port' => '', + 'user' => '', + 'pass' => '', + 'path' => '', + 'query' => [], + 'fragment' => '', + ]; + + protected $_price_format = ''; + protected $_brand_format = ''; + protected $_filter_format = ''; + + + public function __construct($request_url) { + if(defined('PRICE_FILTER_FORMAT') && PRICE_FILTER_FORMAT) { + $this->_price_format = PRICE_FILTER_FORMAT; + } + + if(defined('BRAND_FILTER_FORMAT') && BRAND_FILTER_FORMAT) { + $this->_brand_format = BRAND_FILTER_FORMAT; + } + + if(defined('ATTRIBUTE_FILTER_FORMAT') && ATTRIBUTE_FILTER_FORMAT ) { + $this->_filter_format = ATTRIBUTE_FILTER_FORMAT; + } + + $this->_request_url = $request_url; + $this->_url_elements = Url::parse($request_url); + } + + + public function getAllFilterOptions(array $category_list = [], array $attribute_list = [], array $brand_list = [], array $price_list_options = []) { + + $result = [ + 'category' => $category_list, + 'price' => $this->translatePriceList($price_list_options), + 'brand' => $this->translateBrandList($brand_list), + 'attribute' => $this->translateAttributeList($attribute_list), + ]; + + $selected_categories = array_filter($category_list, function ($item){ return $item['is_selected'];}); + $selected_prices = array_filter($result['price'], function ($item){ return $item['is_selected'];}); + $selected_brands = array_filter($result['brand'], function ($item){ return $item['is_selected'];}); + $selected_attributes = array_filter($result['attribute'], function ($item){ return $item['is_selected'];}); + + $filter_messages = []; + + foreach ($selected_categories as $selected_category) { + $filter_messages[] = [ + 'name' => $selected_category['name'], // 'Danh mục: '. + 'reset_url' => Url::buildUrl($this->_request_url, ['category' => ''] ), + ]; + } + + foreach ($selected_prices as $selected_price) { + $filter_messages[] = [ + 'name' => $selected_price['name'], // 'Giá: '. + 'reset_url' => $selected_price['url'], + ]; + } + + foreach ($selected_brands as $selected_brand) { + $filter_messages[] = [ + 'name' => $selected_brand['name'], // 'Thương hiệu: '. + 'reset_url' => $selected_brand['url'], + ]; + } + + foreach ($selected_attributes as $selected_attribute) { + + $selected_attribute_values = array_filter($selected_attribute['value_list'], function ($item){ return $item['is_selected'];}); + + foreach ($selected_attribute_values as $selected_attribute_value) { + $filter_messages[] = [ + 'name' => $selected_attribute_value['name'], //$selected_attribute['name'].': '.$selected_attribute_value['name'], + 'reset_url' => $selected_attribute_value['url'], + ]; + } + } + + + return [$result, $filter_messages]; + } + + + public function translateAttributeList(array $attribute_list = []) { + $result = []; + + foreach ($attribute_list as $attr_info) { + $copy = $attr_info; + $copy['value_list'] = array_map(function ($value_info) use ($attr_info) { + return $this->translateAttributeValue($attr_info['filter_code'], $value_info); + }, $attr_info['value_list']); + + $result[] = $copy; + } + + return $result; + } + + + protected function translateAttributeValue($attribute_filter_code, array $value_info = []) { + $copy = $value_info; + + if($copy['is_selected']) { + $copy['url'] = Url::buildUrl($this->_request_url, $this->buildAttributeValueUrlResetParameters($attribute_filter_code, $value_info['id'], $value_info['api_key']) ); + }else{ + $copy['url'] = Url::buildUrl($this->_request_url, $this->buildAttributeValueUrlParameters($attribute_filter_code, $value_info['id'], $value_info['api_key']) ); + } + + //$copy['reset_url'] = Url::buildUrl($this->_request_url, $this->buildAttributeValueUrlResetParameters($attribute_filter_code, $value_info['id'], $value_info['api_key']) ); + + return $copy; + } + + + protected function buildAttributeValueUrlParameters($attribute_filter_code, $value_id, $value_api_key) { + if($this->_filter_format == 'filter_code') { + $filter_query = (isset($this->_url_elements['query'][$attribute_filter_code])) ? $this->_url_elements['query'][$attribute_filter_code] : ''; + $filter_value_list = ($filter_query) ? explode(FILTER_VALUE_SEPARATOR, $filter_query) : []; + + $filter_value_list[] = $value_api_key; + + $result = []; + $result[$attribute_filter_code] = join(FILTER_VALUE_SEPARATOR, $filter_value_list ); + + return $result; + } + + // default + $filter_query = (isset($this->_url_elements['query']['filter'])) ? $this->_url_elements['query']['filter'] : ''; + $filter_value_list = ($filter_query) ? explode(FILTER_VALUE_SEPARATOR, $filter_query) : []; + + $filter_value_list[] = $value_id; + sort($filter_value_list); + + return [ + 'filter' => join(FILTER_VALUE_SEPARATOR, $filter_value_list ), + ]; + } + + + protected function buildAttributeValueUrlResetParameters($attribute_filter_code, $value_id, $value_api_key) { + if($this->_filter_format == 'filter_code') { + $filter_query = (isset($this->_url_elements['query'][$attribute_filter_code])) ? $this->_url_elements['query'][$attribute_filter_code] : ''; + $filter_value_list = ($filter_query) ? explode(FILTER_VALUE_SEPARATOR, $filter_query) : []; + + $filter_value_list = remove_item_from_array($filter_value_list, $value_api_key); + + $result = []; + $result[$attribute_filter_code] = join(FILTER_VALUE_SEPARATOR, $filter_value_list ); + + return $result; + } + + + // default + $filter_query = (isset($this->_url_elements['query']['filter'])) ? $this->_url_elements['query']['filter'] : ''; + $filter_value_list = ($filter_query) ? explode(FILTER_VALUE_SEPARATOR, $filter_query) : []; + + $filter_value_list = remove_item_from_array($filter_value_list, $value_id); + + return [ + 'filter' => join(FILTER_VALUE_SEPARATOR, $filter_value_list ), + ]; + } + + + public function translateBrandList(array $brand_list = []) { + + return array_map(function ($item){ + $copy = $item; + + if(($copy['is_selected'])) { + $copy['url'] = Url::buildUrl($this->_request_url, $this->buildBrandUrlResetParameters($item['id'], $item['brand_index']) ) ; + }else{ + $copy['url'] = Url::buildUrl($this->_request_url, $this->buildBrandUrlParameters($item['id'], $item['brand_index']) ); + } + + //$copy['reset_url'] = Url::buildUrl($this->_request_url, $this->buildBrandUrlResetParameters($item['id'], $item['brand_index']) ); + //unset($copy['brand_index']); + + return $copy; + + }, $brand_list); + + } + + + protected function buildBrandUrlResetParameters($brand_id, $brand_index){ + $brand_query = (isset($this->_url_elements['query']['brand'])) ? $this->_url_elements['query']['brand'] : ''; + $brand_list = ($brand_query) ? explode(FILTER_VALUE_SEPARATOR, $brand_query) : []; + + if($this->_brand_format == 'brand_index') { + $brand_list = remove_item_from_array($brand_list, $brand_index); + }else{ + $brand_list = remove_item_from_array($brand_list, $brand_id); + } + + return [ + 'brand' => join(FILTER_VALUE_SEPARATOR, $brand_list ), + ]; + } + + + protected function buildBrandUrlParameters($brand_id, $brand_index){ + $brand_query = (isset($this->_url_elements['query']['brand'])) ? $this->_url_elements['query']['brand'] : ''; + $brand_list = ($brand_query) ? explode(FILTER_VALUE_SEPARATOR, $brand_query) : []; + + if($this->_brand_format == 'brand_index') { + if(!in_array($brand_index, $brand_list)) $brand_list[] = $brand_index; + }else{ + if(!in_array($brand_id, $brand_list)) $brand_list[] = $brand_id; + } + + return [ + 'brand' => join(FILTER_VALUE_SEPARATOR, $brand_list ), + ]; + } + + + public function translatePriceList(array $price_list_options = []) { + + return array_map(function ($item){ + $copy = $item; + + $format = $this->buildPriceRangeFormat($item['min'], $item['max']); + + $copy['name'] = $format['name']; + $copy['url'] = ($copy['is_selected']) ? Url::buildUrl($this->_request_url, $this->buildPriceUrlResetParameters()) : Url::buildUrl($this->_request_url, $format['url_para']); + + //$copy['reset_url'] = Url::buildUrl($this->_request_url, $this->buildPriceUrlResetParameters()); + //unset($copy['min'], $copy['max']); + + return $copy; + + }, $price_list_options); + + } + + + protected function buildPriceRangeFormat($min_price, $max_price) { + if($min_price > 0 && !$max_price) { + $title = "Trên ".$this->format_search_price($min_price); + }elseif(!$min_price && $max_price > 0) { + $title = "Dưới ".$this->format_search_price($max_price); + }else{ + $title = $this->format_search_price($min_price)." - ".$this->format_search_price($max_price); + } + + return [ + 'url_para' => $this->buildPriceUrlParameters($min_price, $max_price), + 'name' => $title, + ]; + } + + + protected function format_search_price($p_price){ + + $price_len = strlen($p_price); + + if($price_len < 7) { + return round($p_price/1000,1)." ngàn"; + } + else if($price_len < 10) { + return round($p_price/1000000,1)." triệu"; + } + + return round($p_price/1000000000,1)." tỷ"; + } + + + protected function buildPriceUrlResetParameters() { + if( $this->_price_format == 'p') { + return [ + "p" => '', + ]; + } + + return [ + "min" => '' , + "max" => '' , + ]; + } + + + protected function buildPriceUrlParameters($min_price = 0, $max_price = 0) { + + if($min_price > 0 && !$max_price) { + if( $this->_price_format == 'p') { + return [ + "p" => "tren-".self::convertPriceFormatToFilter($this->format_search_price($min_price)), + ]; + } + + return [ + "min" => $min_price , + ]; + } + + if(!$min_price && $max_price > 0) { + + if( $this->_price_format == 'p') { + return [ + "p" => "duoi-" . self::convertPriceFormatToFilter($this->format_search_price($max_price)), + ]; + } + + return [ + "max" => $max_price, + ]; + } + + if( $this->_price_format == 'p') { + return [ + "p" => self::convertPriceFormatToFilter($this->format_search_price($min_price)) . "-" . self::convertPriceFormatToFilter($this->format_search_price($max_price)), + ]; + } + + return [ + "max" => $max_price , + "min" => $min_price, + ]; + } + + + // convert: 10 ngàn -> 10ngan, 10 triệu => 10trieu + protected static function convertPriceFormatToFilter($price_format){ + $price_format = str_replace(['ngàn', 'triệu'], ['ngan', 'trieu'], $price_format); + // remove whitespace + return str_replace(" ",'', $price_format); + } + + +} diff --git a/inc/Hura8/Components/Product/Controller/bProductCategoryController.php b/inc/Hura8/Components/Product/Controller/bProductCategoryController.php new file mode 100644 index 0000000..12720e3 --- /dev/null +++ b/inc/Hura8/Components/Product/Controller/bProductCategoryController.php @@ -0,0 +1,62 @@ +objProductCategoryModel = new ProductCategoryModel(); + + if(!$this->isDefaultLanguage()) { + //$this->objProductCategoryLanguageModel->createTableLang(); + parent::__construct( + $this->objProductCategoryModel, + new ProductCategoryLanguageModel() + ); + + }else{ + parent::__construct( + $this->objProductCategoryModel + ); + } + } + + + protected function formatItemInfo(?array $info) : ?array + { + if(!$info) return null; + + if($info['icon']) $info['icon'] = STATIC_DOMAIN . "/" . static::$image_folder . "/" . $info['icon']; + + return $info; + } + + // get full info- basic with description + public function getFullInfo($id): ?array + { + $info = $this->objProductCategoryModel->getFullInfo($id); + + if(!$info) return null; + + if($this->iEntityLanguageModel) { + $item_language_info = $this->iEntityLanguageModel->getInfo($id); + if($item_language_info) { + return $this->formatItemInfo(array_merge($info, $item_language_info)); + } + } + + return $this->formatItemInfo($info); + } + +} diff --git a/inc/Hura8/Components/Product/Controller/bProductCollectionController.php b/inc/Hura8/Components/Product/Controller/bProductCollectionController.php new file mode 100644 index 0000000..d4f6fae --- /dev/null +++ b/inc/Hura8/Components/Product/Controller/bProductCollectionController.php @@ -0,0 +1,62 @@ +objProductCollectionModel = new ProductCollectionModel(); + + if(!$this->isDefaultLanguage()) { + //$this->objProductCategoryLanguageModel->createTableLang(); + parent::__construct( + $this->objProductCollectionModel, + new ProductCollectionLanguageModel() + ); + + }else{ + parent::__construct( + $this->objProductCollectionModel + ); + } + } + + + public function getTotalProduct($collection_id, array $condition = []) + { + return $this->objProductCollectionModel->getTotalProduct($collection_id, $condition); + } + + + public function getListProduct($collection_id, array $condition = []) { + return $this->objProductCollectionModel->getListProduct($collection_id, $condition); + } + + + protected function formatItemInList(array $info) : array + { + return $this->formatItemInfo($info); + } + + + protected function formatItemInfo(?array $info) : ?array + { + $info['url'] = "/collection/".$info['url_index']; + return $info; + } + + +} diff --git a/inc/Hura8/Components/Product/Controller/bProductController.php b/inc/Hura8/Components/Product/Controller/bProductController.php new file mode 100644 index 0000000..d37f093 --- /dev/null +++ b/inc/Hura8/Components/Product/Controller/bProductController.php @@ -0,0 +1,210 @@ + ['width' => 100,] , + 's' => ['width' => 300,] , + 'l' => ['width' => 650,] + ); + + protected $objProductModel; + + public function __construct() + { + $this->objProductModel = new ProductModel(); + + if(!$this->isDefaultLanguage()) { + $objProductLanguageModel = new ProductLanguageModel(); + // $this->objProductLanguageModel->createTableLang(); + parent::__construct($this->objProductModel, $objProductLanguageModel); + }else{ + parent::__construct($this->objProductModel); + } + } + + + //get product image list + public function productImageList($proId){ + $objProductImageModel = new ProductImageModel($proId); + $result = array(); + foreach ( $objProductImageModel->getList(["numPerPage" => 100]) as $rs ) { + $result[] = [ + "size" => static::getResizedImageCollection($rs['img_name']), + "alt" => $rs['alt'], + "folder" => $rs['folder'], + ]; + } + + return $result; + } + + + public function getProductDisplayOptions($current_url) { + $allowed_options = array( + "list" => "Danh sách", + "grid" => "Xem nhóm", + "detail" => "Chi tiết" + ) ; + + $collection = array(); + foreach($allowed_options as $option => $name){ + $url = Url::buildUrl($current_url, array("display" => "")); + $collection[] = array( + "url" => Url::buildUrl($url, array("display" => $option)) , + "key" => $option , + "name" => $name , + ); + } + return $collection; + } + + + public function getProductOtherFilterOptions($current_url) { + $allowed_options = array( + //"order" => $global_language['sort_by_order'] , + "in-stock" => 'Còn hàng' , + ) ; + + $_collection = array(); + foreach($allowed_options as $option => $_name){ + $_collection[] = array( + "url" => Url::buildUrl($current_url, array("other_filter" => $option, 'page' => '')) , + "key" => $option , + "name" => $_name , + ); + } + return $_collection; + } + + + public function getProductSortOptions($current_url, array $options = []) { + //global $global_language; + + if(!sizeof($options)) $options = array( + //"order" => $global_language['sort_by_order'] , + "new" => "Mới nhất" , + "price-asc" => "Giá tăng dần", //$global_language['sort_by_price_asc'] , + "price-desc" => "Giá giảm dần", //$global_language['sort_by_price_desc'] , + "view" => "Lượt xem", //$global_language['sort_by_view'], + "comment" => "Trao đổi", //$global_language['sort_by_comment'] , + "rating" => "Đánh giá", //$global_language['sort_by_rating'] , + "name" => "Tên A->Z" , + ) ; + + $sort_by_collection = array(); + foreach($options as $sort_option=>$sort_name){ + // $url = build_url($current_url, array("sort" => "")); + $sort_by_collection[] = array( + "url" => Url::buildUrl($current_url, array("sort" => $sort_option)) , + "key" => $sort_option , + "name" => $sort_name , + ); + } + return $sort_by_collection; + } + + + public function getProductInfoBySKU($sku) { + + return self::getCache(md5("getFullInfo-".$sku), function () use ($sku) { + + $info = $this->objProductModel->getProductInfoBySKU($sku); + + return ($info) ? $this->formatItemInfo($info) : null; + }); + } + + + // get full info- basic with description + public function getFullInfo($id) + { + return self::getCache("getFullInfo-".$id, function () use ($id) { + + $info = $this->objProductModel->getFullInfo($id); + + if($this->iEntityLanguageModel && $info) { + $item_language_info = $this->iEntityLanguageModel->getInfo($id); + if($item_language_info) { + return $this->formatItemInfo(array_merge($info, $item_language_info)); + } + } + + return ($info) ? $this->formatItemInfo($info) : null; + }); + } + + + protected function validateAndCleanFilterCondition(array $raw_filter_condition): array + { + $clean_values = parent::validateAndCleanFilterCondition($raw_filter_condition); + + // special cases for 'price' which is in range + if(array_key_exists('price', $raw_filter_condition)) { + $value = $raw_filter_condition['price']; + // expect $value = array('max' => 0, 'min'=> 0) + if(isset($value['max']) && isset($value['min'])) { + $clean_values['price'] = array( + 'max' => DataClean::makeInputSafe($value['max'], DataType::INTEGER), + 'min' => DataClean::makeInputSafe($value['min'], DataType::INTEGER), + ); + } + } + + return $clean_values; + } + + + protected function formatItemInList(array $item_info) : array + { + return $this->formatItemInfo($item_info); + } + + + protected function formatItemInfo(?array $item_info): ?array + { + if(!$item_info) return null; + + $info = $item_info; + + $info['image'] = static::getResizedImageCollection($info['thumbnail']); + + return $info; + } + + + public static function getResizedImageCollection($image_name) { + + $image = []; + + $size_in_full = [ + 't' => 'thumb' , + 's' => 'small' , + 'l' => 'large' , + ]; + + foreach (static::$resized_sizes as $size => $value) { + $image[$size_in_full[$size]] = ($image_name) ? STATIC_DOMAIN . "/". static::$image_folder . "/". $size. IMAGE_FILE_SEPARATOR . $image_name : ''; + } + + $image['original'] = ($image_name) ? STATIC_DOMAIN . "/". static::$image_folder . "/". $image_name : ''; + + return $image; + } + +} diff --git a/inc/Hura8/Components/Product/Model/ProductAttributeLanguageModel.php b/inc/Hura8/Components/Product/Model/ProductAttributeLanguageModel.php new file mode 100644 index 0000000..8f2f0b5 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductAttributeLanguageModel.php @@ -0,0 +1,16 @@ +db->runQuery( + "SELECT + a.title, + a.summary, + a.is_filter, + a.value_match_all, + a.filter_code, + a.is_display, + a.is_header, + a.is_multi, a.in_summary, + a.value_count, + g.attr_id, + g.ordering, + g.status + FROM ".$this->tb_attribute_per_spec_group." g, ".$this->tb_entity." a + WHERE g.`group_id`= ? AND g.`attr_id`= a.`id` + ORDER BY g.`ordering` DESC, a.`ordering` DESC ", + ['d'], + [$group_id] + ) ; + + $attribute_ids = []; + $value_per_attribute_ids = []; + $result = []; + foreach ($this->db->fetchAll($query) as $item) { + $attribute_ids[] = $item['attr_id']; + $value_per_attribute_ids[$item['attr_id']] = []; + + $result[$item['attr_id']] = [ + "attribute_info" => $item, + "attribute_values" => &$value_per_attribute_ids[$item['attr_id']], + ]; + } + + + if(!sizeof($attribute_ids)) { + return []; + } + + // now get all values for each attribute + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($attribute_ids, "int"); + + $query = $this->db->runQuery( + "SELECT * FROM ".$this->tb_attribute_value." + WHERE `attribute_id` IN (".$parameterized_ids.") + ORDER BY `ordering` DESC, `title` ASC ", + $bind_types, + $attribute_ids + ) ; + + foreach ($this->db->fetchAll($query) as $item) { + $value_per_attribute_ids[$item['attribute_id']][] = $item; + } + + return $result; + } + + + public function getSpecGroupAttribute($group_id) { + + $query = $this->db->runQuery( + "SELECT + a.title, + a.summary, + a.is_filter, + a.value_match_all, + a.filter_code, + a.is_display, + a.is_header, a.is_multi, a.value_count, + g.attr_id, + g.ordering, + g.status + FROM ".$this->tb_attribute_per_spec_group." g, ".$this->tb_entity." a + WHERE g.`group_id`= ? AND g.`attr_id`= a.`id` + ORDER BY g.`ordering` DESC, a.`ordering` DESC ", + ['d'], + [$group_id] + ) ; + + return $this->db->fetchAll($query); + } + + + public function getProductMatchAllAttributeValueIds(array $_att_value_list) { + /*$query = $this->db->runQuery(" + SELECT `pro_id` , COUNT(*) AS num_pro + FROM ".TB_PRODUCT_ATTRIBUTE." + WHERE " . join(" OR ", array_map(function ($_item){ return " `attr_value_id` = '".intval($_item['id'])."' "; }, $_att_value_list )) ." + GROUP BY pro_id + HAVING num_pro = ".sizeof($_att_value_list)." + LIMIT 10000 + "); + + return array_map(function ($item){ + return $item["pro_id"]; + }, $this->db->fetchAll($query));*/ + + return []; + } + + public function getProductMatchAttributeValue(array $value_ids) { + $product_filter_id_match = array(); + // todo: + /*if(sizeof($query_attr_id)) { + $query = $this->db->runQuery(" + SELECT DISTINCT pro_id , COUNT(*) as num_pro + FROM ".TB_PRODUCT_ATTRIBUTE." + WHERE attr_value_id IN (".join(',', $query_attr_id).") + GROUP BY pro_id + HAVING num_pro = ".$count_filter." + LIMIT 10000 + "); + foreach ( $this->db->fetchAll($query) as $rs ) { + $product_filter_id_match[] = $rs["pro_id"]; + } + }*/ + + return $product_filter_id_match; + } + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + /*$condition = array( + "q" => "", + "letter" => "", + "status" => 0, + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductAttributeValueModel.php b/inc/Hura8/Components/Product/Model/ProductAttributeValueModel.php new file mode 100644 index 0000000..4efdb01 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductAttributeValueModel.php @@ -0,0 +1,83 @@ +attribute_id = $attribute_id; + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function getProductAttributes($product_id) { + + $query = $this->db->runQuery( + "SELECT `attr_id`, `attr_value_id` FROM `".$this->tb_product_attribute."` WHERE `pro_id` = ? ", + ['d'], [ $product_id] + ); + + return $this->db->fetchAll($query); + } + + + protected function getAttributeValueCount() { + $query = $this->db->runQuery( + "SELECT `value_count` FROM `".$this->tb_attribute."` WHERE `id` = ? LIMIT 1 ", + ['d'], [$this->attribute_id] + ); + + if($info = $this->db->fetchAssoc($query)) { + return $info['value_count']; + } + + return 0; + } + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + + $catCondition = [" AND `attribute_id` = ? "]; + $bind_types = ["d"]; + $bind_values = [$this->attribute_id]; + + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + public function getInfoByCode($filter_code) + { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_entity."` WHERE `attribute_id` = ? AND `filter_code` = ? LIMIT 1", + ['d', 's'], [$this->attribute_id, $filter_code ] + ); + + return $this->db->fetchAssoc($query); + } + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductCategoryInfoModel.php b/inc/Hura8/Components/Product/Model/ProductCategoryInfoModel.php new file mode 100644 index 0000000..6af77c4 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductCategoryInfoModel.php @@ -0,0 +1,30 @@ +db = get_db(); + } + + public function getInfo($id) { + return $this->db->select( + $this->tb_category_info, + [], + [ + 'id' => ['=', $id], + ] + ); + } + + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductCategoryLanguageModel.php b/inc/Hura8/Components/Product/Model/ProductCategoryLanguageModel.php new file mode 100644 index 0000000..f8dd0e5 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductCategoryLanguageModel.php @@ -0,0 +1,21 @@ +richtext_fields); + } + +} diff --git a/inc/Hura8/Components/Product/Model/ProductCategoryModel.php b/inc/Hura8/Components/Product/Model/ProductCategoryModel.php new file mode 100644 index 0000000..fe67f3a --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductCategoryModel.php @@ -0,0 +1,83 @@ +db->runQuery("SELECT + a.id , + a.filter_code , + a.title , + a.value_count , + a.is_filter , + c.ordering , + c.status , + a.is_header + FROM ".$this->tb_attribute." a + LEFT JOIN ".$this->tb_attribute_per_category." c ON a.id = c.attr_id + WHERE c.category_id = ? + ORDER BY c.ordering desc + ", ['d'], [ $catId ]) ; // AND isSearch = 1 + + return $this->db->fetchAll($query); + } + + + public function getFullInfo($id) : ?array { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_entity."` basic, `".$this->tb_category_info."` info + WHERE basic.`id` = info.`id` AND basic.id = ? + LIMIT 1 ", + ['d'], [$id] + ); + + if( $item_info = $this->db->fetchAssoc($query)){ + $item_info['settings'] = ($item_info['settings']) ? \json_decode($item_info['settings'], true) : []; + return $item_info; + } + + return null; + } + + + public function getEmptyInfo($addition_field_value = []) : array + { + // basic table + $basic_empty = parent::getEmptyInfo($addition_field_value); + + // info table + foreach ($this->db->getTableInfo($this->tb_category_info) as $field => $field_info) { + $basic_empty[$field] = ( in_array($field_info['DATA_TYPE'], ['int', 'float']) ) ? 0 : '' ; + } + + if(sizeof($addition_field_value)) { + return array_merge($basic_empty, $addition_field_value); + } + + return $basic_empty; + } + +} diff --git a/inc/Hura8/Components/Product/Model/ProductCollectionLanguageModel.php b/inc/Hura8/Components/Product/Model/ProductCollectionLanguageModel.php new file mode 100644 index 0000000..30c85a8 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductCollectionLanguageModel.php @@ -0,0 +1,22 @@ +richtext_fields); + + //$this->createTableLang(); + } + +} diff --git a/inc/Hura8/Components/Product/Model/ProductCollectionModel.php b/inc/Hura8/Components/Product/Model/ProductCollectionModel.php new file mode 100644 index 0000000..cda3041 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductCollectionModel.php @@ -0,0 +1,85 @@ +db->runQuery( + " SELECT * FROM `".$this->tb_entity."` WHERE `url_index` = ? LIMIT 1", + ['s'], [$url] + ); + + return $this->db->fetchAssoc($query); + } + + public function getTotalProduct($collection_id, array $condition = []) + { + $query = $this->db->runQuery( + " SELECT COUNT(*) as total FROM `".$this->tb_collection_product."` WHERE `collection_id` = ? ", + ['d'], [$collection_id] + ); + + $total = 0; + if ($rs = $this->db->fetchAssoc($query)) { + $total = $rs['total']; + } + + return $total; + } + + + public function getListProduct($collection_id, array $condition = []) + { + $numPerPage = (isset($condition['numPerPage']) && $condition['numPerPage'] > 0 ) ? intval($condition['numPerPage']) : 20 ; + $page = (isset($condition['page']) && $condition['page'] > 0 ) ? intval($condition['page']) : 1 ; + $order_by = " `ordering` DESC, `id` DESC"; + + $query = $this->db->runQuery( + "SELECT `product_id`, `ordering` FROM ".$this->tb_collection_product." WHERE `collection_id` = ? + ORDER BY ".$order_by." + LIMIT ".(($page-1) * $numPerPage).", ".$numPerPage , + ['d'], [$collection_id] + ) ; + + $item_list = array(); + $counter = ($page-1) * $numPerPage; + foreach ( $this->db->fetchAll($query) as $item ) { + $counter += 1; + + $item_list[$item['product_id']] = [ + 'counter' => $counter, + 'ordering' => $item['ordering'], + ]; + } + + $objProductModel = new ProductModel(); + $product_list_info = $objProductModel->getListByIds(array_keys($item_list)); + + // final list + $final_list = []; + foreach ($item_list as $_pro_id => $_pro_info_in_collection) { + $pro_basic = $product_list_info[$_pro_id] ?? null; + if($pro_basic) { + $final_list[] = array_merge($pro_basic, $_pro_info_in_collection); + } + } + + return $final_list; + } + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductFilterModel.php b/inc/Hura8/Components/Product/Model/ProductFilterModel.php new file mode 100644 index 0000000..2aca558 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductFilterModel.php @@ -0,0 +1,452 @@ +db = get_db('', ENABLE_DB_DEBUG); + } + + + public function findProductListIdMatchFilters(array $SQLConditionFromFilters) { + list( + $where_query, + $current_page_id, + $number_per_page, + $ordering_clause + ) = $SQLConditionFromFilters; + + $db_condition = join(" ", $where_query); + + $total_number = 0; + $query = $this->db->runQuery("SELECT COUNT(`id`) AS total FROM ".TableName::PRODUCT." WHERE 1 ".$db_condition." "); + if($rs = $this->db->fetchAssoc($query)) { + $total_number = $rs['total']; + } + + $query = $this->db->runQuery(" + SELECT * FROM ".TableName::PRODUCT." + WHERE 1 ".$db_condition." + ".$ordering_clause." + LIMIT ". ($current_page_id - 1) * $number_per_page .", ".$number_per_page." + "); + + $item_list = $this->db->fetchAll($query); + + return [$total_number, $item_list]; + } + + + public function ratingList(array $list_product_ids) { + + if(!sizeof($list_product_ids)) return []; + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($list_product_ids, 'int'); + + $query = $this->db->runQuery( + " + SELECT `review_rate`, COUNT(`id`) as num + FROM ".TableName::PRODUCT." + WHERE `id` IN (".$parameterized_ids.") + GROUP BY `review_rate` + ORDER BY review_rate ASC + ", + $bind_types, + $list_product_ids + ); + + return $this->db->fetchAll($query); + } + + + public function categoryList(array $current_category_ids, array $list_product_ids) { + if(!sizeof($list_product_ids)) { + return []; + } + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($list_product_ids, 'int'); + + $query = $this->db->runQuery( + " + SELECT `category_id`, COUNT(`item_id`) AS numPro + FROM ".TableName::PRODUCT_PER_CATEGORY." + WHERE `item_id` IN (".$parameterized_ids.") + GROUP BY `category_id` ", + $bind_types, + $list_product_ids + ); + + //lay ket qua dem + $result_count = []; + foreach ($this->db->fetchAll($query) as $rs ){ + $result_count[$rs['category_id']] = $rs['numPro']; + } + + if(sizeof($result_count)) { + + $objProductCategoryModel = new ProductCategoryModel(); + + $final_result = []; + foreach ($objProductCategoryModel->getListByIds(array_keys($result_count)) as $rs ){ + + $rs['count'] = $result_count[$rs['id']]; + $rs['is_selected'] = in_array($rs['id'], $current_category_ids); + + $final_result[] = $rs; + } + + return $final_result; + } + + return []; + } + + + public function brandList(array $current_brand_ids, array $list_product_ids) { + + if(!sizeof($list_product_ids)) { + return []; + } + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($list_product_ids, 'int'); + + $query = $this->db->runQuery( + " SELECT `brand_id`, COUNT(`id`) AS total + FROM ".TableName::PRODUCT." + WHERE `id` IN (".$parameterized_ids.") + GROUP BY `brand_id` + ORDER BY total DESC + LIMIT 100", + $bind_types, + $list_product_ids + ); + + + $neededList = array(); + foreach ( $this->db->fetchAll($query) as $result ) { + $neededList[$result['brand_id']] = array( + "id" => $result['brand_id'] , + "count" => $result['total'] , + ); + } + + $brand_list_ids[] = array_keys($neededList); + $result_list = array(); + + if(sizeof($brand_list_ids)) { + + $objBrandModel = new BrandModel(); + + foreach ( $objBrandModel->getListByIds($brand_list_ids) as $_brand ) { + + $item_count = (isset($neededList[$_brand['id']])) ? $neededList[$_brand['id']]['count'] : 0; + + $_brand['count'] = $item_count; + $_brand["is_selected"] = in_array($_brand['id'], $current_brand_ids); + + + $result_list[$_brand['id']] = $_brand; + } + } + + return array_values($result_list); + } + + + public function attributeList(array $current_filter_attribute_value_ids, array $current_category_ids, array $list_product_ids) { + if(!sizeof($list_product_ids)) { + return []; + } + + $category_attribute_filter_array = $this->countNumProPerAttributeForCategory($current_category_ids, $list_product_ids); + + if(!sizeof($category_attribute_filter_array)) { + return []; + } + + $filter_box = array(); + + foreach($category_attribute_filter_array as $attributeIdGroup){ + + $attribute_value_array = $attributeIdGroup['value_list']; + $attribute_name = $attributeIdGroup['name']; + $attribute_selected = 0; + $filterList = array(); + + $count_num = 0; + + foreach($attribute_value_array as $att_value_id => $rs_value){ + $att_value_name = $rs_value["info"]["name"]; + $att_value_description = $rs_value["info"]["description"]; + $count_pro = $rs_value["pro_count"]; + $is_selected = in_array($att_value_id, $current_filter_attribute_value_ids); + if($is_selected) { + $attribute_selected = 1; + } + + unset($this_filter_query, $this_filter_col_value); + + //gioi han danh sach + if($count_num > 50) { + break; + } + + $count_num++; + + $filterList[] = array( + "id" => $att_value_id , + "name" => $att_value_name , + 'api_key' => $rs_value["info"]["api_key"], + "description" => $att_value_description, + "count" => $count_pro , + "is_selected" => $is_selected, + ); + } + + if($count_num > 0){ + $filter_box[] = [ + "name" => $attribute_name, + 'filter_code' => $attributeIdGroup['filter_code'], + "is_selected" => $attribute_selected, + "value_list" => $filterList , + ]; + } + + } + + return $filter_box; + } + + + protected function countNumProPerAttributeForCategory(array $current_category_ids, array $list_product_ids){ + + if(!sizeof($list_product_ids)) { + return []; + } + + $category_attributes = $this->getAttributesForCategory($current_category_ids); + $att_value_id_list = []; + foreach ($category_attributes as $attribute) { + $att_value_id_list = array_merge($att_value_id_list, array_keys($attribute['value_list'])); + } + + // count product per attribute-value-id + $result_count = array(); + if(sizeof($att_value_id_list)) { + $query = $this->db->runQuery(" + SELECT + pro_att.`attr_value_id`, + COUNT(DISTINCT ".TB_PRODUCT_CATEGORY.".pro_id) AS numPro + FROM ".TB_PRODUCT_ATTRIBUTE." pro_att + LEFT JOIN ".TB_PRODUCT_CATEGORY." ON pro_att.pro_id = ".TB_PRODUCT_CATEGORY.".pro_id + WHERE pro_att.pro_id IN (".join(',', $list_product_ids ).") + AND pro_att.`attr_value_id` IN (".join(",", $att_value_id_list).") + GROUP BY pro_att.`attr_value_id` "); + + //lay ket qua dem + foreach ($this->db->fetchAll($query) as $rs ){ + $result_count[$rs['attr_value_id']] = $rs['numPro']; + } + } + + $final_result = []; + foreach ($category_attributes as $attribute) { + $new_value_list = []; + foreach ($attribute['value_list'] as $_value_id => $_value_info) { + $pro_count = isset($result_count[$_value_id]) ? $result_count[$_value_id] : 0; + if($pro_count) { + $_value_info['pro_count'] = $pro_count; + $new_value_list[$_value_id] = $_value_info; + } + } + $attribute['value_list'] = $new_value_list; + $final_result[] = $attribute; + } + + return $final_result; + } + + + // 15-07-2020 get all attributes and values for a list of category + protected function getAttributesForCategory(array $category_ids){ + + if(!sizeof($category_ids)) { + return array(); + } + + + $final_result = array();//array to return + + $category_att_id_list = array(); + $attribute_array = array(); //keep & re-order attribute + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($category_ids, 'int'); + + $query = $this->db->runQuery(" + SELECT `attr_id`, `ordering` FROM `idv_attribute_category` + WHERE `category_id` IN (". $parameterized_ids .") AND `status` = 1 ", $bind_types, $category_ids); + + foreach ($this->db->fetchAll($query) as $rs ){ + $category_att_id_list[] = $rs['attr_id']; + $attribute_array[$rs['attr_id']] = $rs['ordering']; + } + + if(!sizeof($category_att_id_list)) { + return array(); + } + + + $att_value_id_list = array(); + $attribute_info = array(); + $attribute_att_value_item = array(); + $attribute_value_info = array(); + $query = $this->db->runQuery(" + SELECT + attval.id , + attval.attributeId , + attval.value , + attval.description , + attval.api_key , + attval.value_en , + att.attribute_code , + att.filter_code, + att.name , + att.ordering + FROM ".TB_ATTRIBUTE_VALUE." attval + LEFT JOIN ".TB_ATTRIBUTE." att ON attval.attributeId = att.id + WHERE att.id IN (".join(',', $category_att_id_list).") AND att.isSearch = 1 + ORDER BY attval.ordering DESC "); + + foreach ($this->db->fetchAll($query) as $rs ){ + $att_value_id = $rs['id']; + $att_id = $rs['attributeId']; + + $att_value_id_list[] = $att_value_id; + + $attribute_att_value_item[$att_id][] = $att_value_id; //in ordering + + $attribute_info[$att_id] = array( + "id" => $att_id, + "name" => $rs['name'], + "code" => $rs['attribute_code'], + "filter_code" => $rs['filter_code'], + ); + $attribute_value_info[$att_value_id] = array( + "id" => $att_value_id, + "name" => $rs['value'], + "description" => $rs['description'], + "api_key" => $rs['api_key'], + ); + } + + //cho ra ket qua + arsort($attribute_array); //sap xep thu tu cua attribute theo ordering + + foreach($attribute_array as $att_id => $att_order){ + $this_attribute_atr_value_item = array(); + if(isset($attribute_att_value_item[$att_id])) { + foreach($attribute_att_value_item[$att_id] as $att_value_id){ + $this_attribute_atr_value_item[$att_value_id] = array( + "id" => $att_value_id, + "info" => $attribute_value_info[$att_value_id], + ); + } + } + + if(isset($attribute_info[$att_id])) { + $info = $attribute_info[$att_id]; + $info['value_list'] = $this_attribute_atr_value_item; + + $final_result[] = $info; + } + } + + return $final_result; + } + + + public function priceList($current_max_price, $current_min_price, array $list_product_ids, array $category_price_range) { + + if(!sizeof($list_product_ids)) { + return []; + } + + $total_prices = sizeof($category_price_range); + if( ! $total_prices ){ + return []; + } + + //pad head and tail with 0 + array_unshift($category_price_range, 0); + $category_price_range[] = 0; + + $result = array(); + $queries = []; + + for($i = 0; $i <= $total_prices ; $i++){ + + $range_key = md5($category_price_range[$i] .'-'. $category_price_range[$i+1] ); + $queries[] = $this->buildRangeQuery($list_product_ids, $range_key, $category_price_range[$i], $category_price_range[$i+1]); + + $min_price = intval($category_price_range[$i]); + $max_price = intval($category_price_range[$i+1]); + $is_selected = ($max_price == $current_max_price && $min_price == $current_min_price ); + + $result[$range_key] = array( + "min" => $min_price , + "max" => $max_price, + "count" => 0 , + "is_selected" => ($is_selected) ? 1 : 0 , + ); + + } + + $query = $this->db->runQuery(" SELECT `range_key`, `total` FROM ( ".join(' UNION ALL ', $queries)." ) AS tmp"); + foreach ($this->db->fetchAll($query) as $rs) { + $result[$rs['range_key']]['count'] = $rs['total']; + } + + // get only range with product-count > 0 + return array_values(array_filter( $result , function ($item){ + return $item['count'] > 0; + })); + } + + + private function buildRangeQuery(array $list_product_ids, $range_key, $minPrice, $maxPrice){ + + $minPrice = (int) $minPrice; + $maxPrice = (int) $maxPrice; + if($minPrice < 10) $minPrice = 10; + + $new_price_range = " AND `id` IN (".join(',', $list_product_ids ).") "; + + if($maxPrice > 0 && $minPrice > 0){ + $new_price_range .= " AND ( `price` BETWEEN '".$minPrice."' AND '".$maxPrice."' ) "; + }elseif($maxPrice > 0){ + $new_price_range .= " AND `price` < '".$maxPrice."' "; + }elseif($minPrice > 0 && $maxPrice == 0){ + $new_price_range .= " AND `price` >='".$minPrice."' "; + } + + return " SELECT '".$range_key."' AS `range_key`, COUNT(`id`) AS total + FROM ".TableName::PRODUCT." + WHERE 1 ".$new_price_range; + } + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductHotModel.php b/inc/Hura8/Components/Product/Model/ProductHotModel.php new file mode 100644 index 0000000..3984cc0 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductHotModel.php @@ -0,0 +1,95 @@ +db = get_db(); + } + + public function updateProductHot($pro_id, array $new_types) { + + $this->db->runQuery("DELETE FROM " . $this->tb_product_hot . " WHERE `pro_id` = ? ", ['d'], [ $pro_id ]); + $this->db->update( + $this->tb_product, + [ + "hot_type" => '', + ], + [ + 'id' => $pro_id, + ] + ); + + + $config_hottype = Config::getProductHotTypeList(); + + //insert what good + $batch_insert = array(); + $accepted_types = array(); + foreach ($new_types as $hot_type) { + if (!isset($config_hottype[$hot_type])) continue; + + $batch_insert[] = [ + "pro_id" => $pro_id, + "hot_type" => $hot_type, + ]; + + $accepted_types[] = $hot_type; + } + + if (sizeof($batch_insert)) { + + $this->db->bulk_insert($this->tb_product_hot, $batch_insert ); + + $this->db->update( + $this->tb_product, + [ + "hot_type" => join(",", $accepted_types), + ], + [ + "id" => $pro_id, + ] + ); + } + + return true; + } + + + public function getProductHot(array $product_ids) { + + if(!sizeof($product_ids)){ + return []; + } + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($product_ids, 'int'); + + $query = $this->db->runQuery( + "SELECT `pro_id`, `hot_type` FROM ".$this->tb_product_hot." WHERE `pro_id` IN (".$parameterized_ids.") ", + $bind_types, + $product_ids + ); + + $result = []; + foreach ( $this->db->fetchAll($query) as $rs ) { + $result[$rs['pro_id']][] = $rs['hot_type']; + } + + return $result; + } + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductImageModel.php b/inc/Hura8/Components/Product/Model/ProductImageModel.php new file mode 100644 index 0000000..cc12988 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductImageModel.php @@ -0,0 +1,78 @@ +product_id = $product_id; + $this->objProductModel = new ProductModel(); + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function setProduct($product_id) { + $this->product_id = $product_id; + } + + public function countProductImage($product_id) { + $query = $this->db->runQuery("SELECT COUNT(*) AS total FROM `".$this->tb_entity."` WHERE `pro_id` = ? ", ['d'], [ $product_id]); + if($info = $this->db->fetchAssoc($query)) { + return $info['total']; + } + + return 0; + } + + + protected function _buildQueryOrderBy($sort_by = "new") + { + return " `ordering` DESC, `id` DESC "; + } + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + /*$condition = array( + "q" => "", + "letter" => "", + "status" => 0, + );*/ + + $catCondition = [" AND `pro_id` = ? "]; + $bind_types = ['d']; + $bind_values = [$this->product_id]; + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function getProductMainImage() { + $query = $this->db->runQuery( + "SELECT `id` FROM `".$this->tb_entity."` WHERE `pro_id` = ? AND `is_main` = 1 LIMIT 1", + ['d'], [ $this->product_id] + ); + + return $this->db->fetchAssoc($query); + } + +} diff --git a/inc/Hura8/Components/Product/Model/ProductInfoModel.php b/inc/Hura8/Components/Product/Model/ProductInfoModel.php new file mode 100644 index 0000000..0c8092a --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductInfoModel.php @@ -0,0 +1,35 @@ +richtext_fields); + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + return null; + } + +} diff --git a/inc/Hura8/Components/Product/Model/ProductLanguageModel.php b/inc/Hura8/Components/Product/Model/ProductLanguageModel.php new file mode 100644 index 0000000..ce8701a --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductLanguageModel.php @@ -0,0 +1,21 @@ +richtext_fields); + } + +} diff --git a/inc/Hura8/Components/Product/Model/ProductModel.php b/inc/Hura8/Components/Product/Model/ProductModel.php new file mode 100644 index 0000000..0fc53d7 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductModel.php @@ -0,0 +1,410 @@ +richtext_fields + ); + } + + + protected function extendedFilterOptions() : array + { + return [ + "price" => array('max' => 0, 'min'=> 0), + "brand" => array(), // array(1,2,3,) + "collection" => array(), // array(1,2,3,) + "supplier" => array(), // array(1,2,3,) + "rating" => array(), // array(1,2,3,) + "category" => array(), // array(1,2,3,) + "status" => array(), // array(1,2,3,) + "hotType" => array(),// array(saleoff | not | new) + "attribute" => array(), // array(1,2,3,) + "promotion" => "", + "other_filter" => array(), // array(in-stock, has-promotion etc...) + "spec_group_id" => array(), // array(1,2,3,) + ]; + } + + + public function getProductInfoBySKU(string $sku) { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_entity."` basic, `".$this->tb_product_info."` info + WHERE basic.`id` = info.`id` AND basic.sku = ? + LIMIT 1 ", + ['d'], [$sku] + ); + + if( $item_info = $this->db->fetchAssoc($query)){ + return $item_info; + } + + return false; + } + + + public function getFullInfo($id) { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_entity."` basic, `".$this->tb_product_info."` info + WHERE basic.`id` = info.`id` AND basic.id = ? + LIMIT 1 ", + ['d'], [$id] + ); + + if( $item_info = $this->db->fetchAssoc($query)){ + return $item_info; + } + + return null; + } + + + public function getProductCategoryList($pro_id) { + $query = $this->db->runQuery( + "SELECT `category_id` FROM ".$this->tb_product_per_category." WHERE `item_id` = ? ", + ['d'], [ $pro_id ] + ) ; + + $pro_cat = []; + foreach ( $this->db->fetchAll($query) as $rs ) { + $pro_cat[] = $rs['category_id']; + } + + return $pro_cat; + } + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + $where_query = []; + $bind_types = []; + $bind_values = []; + + //------------------- + + //other filters + if( array_key_exists("other_filter", $filter_condition) && sizeof($filter_condition["other_filter"])) { + foreach ($filter_condition["other_filter"] as $_filter) { + switch ($_filter) { + case "in-stock"; + $where_query[] = " AND quantity > 0 "; + break; + case "has-vat"; + $where_query[] = " AND `has_vat` = 1 "; + break; + case "out-stock"; + $where_query[] = " AND `quantity` = 0 "; + break; + case "has-market-price"; + $where_query[] = " AND `market_price` > 0 "; + break; + case "no-price"; + $where_query[] = " AND `price` = 0 "; + break; + case "no-warranty"; + $where_query[] = " AND LENGTH(`warranty`) < 2 "; + break; + case "no-sku"; + $where_query[] = " AND LENGTH(`sku`) < 2 "; + break; + case "has-config"; + $where_query[] = " AND `config_count` > 0 "; + break; + case "no-image"; + $where_query[] = " AND `image_count` = 0 "; + break; + case "no-category"; + $where_query[] = " AND `category` = '0' "; + break; + case "display-off"; + $where_query[] = " AND `status`=0 "; + break; + case "display-on"; + $where_query[] = " AND `status`=1 "; + break; + case "has-promotion": + $where_query[] = " AND LENGTH(`special_offer`) < 5 "; + break; + //...add more + } + } + } + + + //- brand id or ids or brand_indexes + if( array_key_exists("brand", $filter_condition) && sizeof($filter_condition["brand"])) { + + $condition = array(); + foreach ($filter_condition["brand"] as $brand_id) { + if(!intval($brand_id)) continue; + $condition[] = " brand_id = ? "; + $bind_types[] = 'd'; + $bind_values[] = $brand_id; + } + + if(sizeof($condition)) { + $where_query[] = " AND ( ".join(" OR ", $condition)." )"; + } + } + + + //- collection id or ids + if( array_key_exists("collection", $filter_condition) && sizeof($filter_condition["collection"])) { + $condition = array(); + + foreach ($filter_condition["collection"] as $_id) { + if(!intval($_id)) continue; + + $condition[] = " `id` IN ( SELECT `product_id` FROM ".$this->tb_collection_product." WHERE `collection_id` = ? ) "; + $bind_types[] = 'd'; + $bind_values[] = $_id; + } + + $where_query[] = " AND ( ".join(" OR ", $condition)." )"; + } + + + //- rating: 1-> 5 + if( array_key_exists("rating", $filter_condition) && sizeof($filter_condition["rating"])) { + $condition = array(); + foreach ($filter_condition["rating"] as $_id) { + $condition[] = " `rating` = ? "; + $bind_types[] = 'd'; + $bind_values[] = $_id; + } + + $where_query[] = " AND ( ".join(" OR ", $condition)." )"; + } + + + //- category id or ids + if( array_key_exists("category", $filter_condition) && sizeof($filter_condition["category"])) { + + $objProductCategoryModel = new ProductCategoryModel(); + + $condition = array(); + foreach ($filter_condition["category"] as $cat_id) { + + $cat_info = $objProductCategoryModel->getInfo($cat_id); + if(!$cat_info) continue; + + if($cat_info["is_parent"]) { + $childListId = ($cat_info["child_ids"]) ?: '0'; + $condition[] = " `category_id` IN (".$childListId .") "; + }else{ + $condition[] = " `category_id` = ? "; + $bind_types[] = 'd'; + $bind_values[] = $cat_id; + } + } + + if(sizeof($condition)) { + $where_query[] = " AND `id` IN ( SELECT DISTINCT `item_id` FROM ".$this->tb_product_per_category." WHERE " . join(" OR ", $condition) . " )"; + } + } + + + // spec_group_id + if( array_key_exists("spec_group_id", $filter_condition) && sizeof($filter_condition["spec_group_id"])) { + + $condition = array(); + foreach ($filter_condition["spec_group_id"] as $_id) { + if(!$_id) continue; + + $condition[] = " `spec_group_id` = ? "; + $bind_types[] = 'd'; + $bind_values[] = $_id; + } + + $where_query[] = " AND ( ".join(" OR ", $condition )." ) "; + } + + + //- hotType: saleoff | not | new | or combination of types + if( array_key_exists("hotType", $filter_condition) && sizeof($filter_condition["hotType"])) { + $config_hottype = Config::getProductHotTypeList(); + $condition = array(); + foreach ($filter_condition["hotType"] as $_id) { + if(!array_key_exists($_id, $config_hottype)) continue; + + $condition[] = " `hot_type` = ? "; + $bind_types[] = 's'; + $bind_values[] = $_id; + } + + if(sizeof($condition)) { + $where_query[] = " AND `id` IN (SELECT `pro_id` FROM ".$this->tb_product_hot." WHERE 1 AND ( ".join(" OR ", $condition)." ) ) "; + } + + } + + + //- attribute values + /* + if( array_key_exists("attribute", $filter_condition) && sizeof($filter_condition["attribute"])) { + $filter_attr_value_list = $filter_condition["attribute"]; + //filter = attr_value_1-attr_value_2-attr_value_3, + $query_attr_id = []; + $count_filter = 0; + + foreach($filter_attr_value_list as $attr_id){ + $attr_id = (int) $attr_id; + if($attr_id) { + $query_attr_id[] = $attr_id; + $count_filter ++; + } + } + + $product_filter_id_match = array(); + if(sizeof($query_attr_id)) { + $query = $this->db->runQuery(" + SELECT DISTINCT pro_id , COUNT(*) as num_pro + FROM ".TB_PRODUCT_ATTRIBUTE." + WHERE attr_value_id IN (".join(',', $query_attr_id).") + GROUP BY pro_id + HAVING num_pro = ".$count_filter." + LIMIT 10000 + "); + foreach ( $this->db->fetchAll($query) as $rs ) { + $product_filter_id_match[] = $rs["pro_id"]; + } + } + + $where_query[] = (sizeof($product_filter_id_match)) ? " AND `id` IN (".join(', ', $product_filter_id_match).") " : " AND `id` = 0 " ; + + //xay lai url de back + if(!$this->objCategoryProduct) $this->objCategoryProduct = new CategoryProduct(); + + foreach($filter_attr_value_list as $value_id ){ + $att_name = $this->objCategoryProduct->atrValueName($value_id); + + $filterPath["attribute"][] = array( + "id" => $value_id, + "name" => $att_name, + ); + $filter_messages[] = [ + 'title' => $att_name, + 'reset' => Url::buildUrl($this->_request_url, ['filter' => join(FILTER_VALUE_SEPARATOR, remove_item_from_array($filter_condition["attribute"], $value_id))] ), + ]; + } + + } + + */ + + //- attribute values + if( array_key_exists("attribute", $filter_condition) && sizeof($filter_condition["attribute"])) { + + $filter_attr_value_list = $filter_condition["attribute"]; + + $attr_values_per_attribute = $this->groupAttributeValuesByAttributeId($filter_attr_value_list); + + $having_match_count = sizeof(array_keys($attr_values_per_attribute)); + + $attribute_info_list = $this->groupAttributeListInfo(array_keys($attr_values_per_attribute)); + + $attr_where_query = []; + foreach ($attr_values_per_attribute as $_att_id => $_att_value_list) { + // if same attribute : find products that match either ONE of these values (not match ALL values) + $_att_info = $attribute_info_list[$_att_id]; + if (!$_att_info) continue; + + // this attribute requires all products must have ALL selected values + if ($_att_info['value_match_all'] && sizeof($_att_value_list) > 1) { + + $_product_id_match_all = $this->getProductMatchAllAttributeValueIds($_att_value_list); + + if (sizeof($_product_id_match_all) > 0) { + $attr_where_query[] = " ( `id` IN (" . join(",", $_product_id_match_all) . ") ) "; + } else { + $attr_where_query[] = " ( `id` = -1 ) "; + } + + } else { + $attr_where_query[] = " ( SELECT DISTINCT `pro_id` FROM " . TB_PRODUCT_ATTRIBUTE . " WHERE (" . + join(" OR ", array_map(function ($_item) { return " `attr_value_id` = '" . intval($_item['id']) . "' "; }, $_att_value_list)) + . " ) ) "; + } + + } + + $product_filter_id_match = array(); + + if (sizeof($attr_where_query)) { + $query = $this->db->runQuery(" + SELECT `pro_id` , COUNT(*) AS num_pro + FROM ( " . join(" UNION ALL ", $attr_where_query) . " ) AS tpm_tb + GROUP BY pro_id + HAVING num_pro = " . $having_match_count . " + LIMIT 10000 + "); + foreach ($this->db->fetchAll($query) as $rs) { + $product_filter_id_match[] = $rs["pro_id"]; + } + } + + $where_query[] = (sizeof($product_filter_id_match)) ? " AND " . TB_PRODUCT_LIGHT . ".`id` IN (" . join(', ', $product_filter_id_match) . ") " : " AND " . TB_PRODUCT_LIGHT . ".`id` = 0 "; + + } + + + //- price range + if( + isset($filter_condition["price"]) + && sizeof($filter_condition["price"]) + && ($filter_condition["price"]['min'] > 0 || $filter_condition["price"]['max'] > 0) + ) { + + //limit by price range + $maxPrice = DataClean::makeInputSafe($filter_condition["price"]['max'], DataType::INTEGER); + $minPrice = DataClean::makeInputSafe($filter_condition["price"]['min'], DataType::INTEGER); + + $price_range_query = ''; + if($maxPrice > 0 && $minPrice > 0){ + $price_range_query = " ( `price` BETWEEN '".$minPrice."' AND '".$maxPrice."' ) "; + + }else if($maxPrice > 0){ + $price_range_query = " `price` < '".$maxPrice."' "; + + }else if($minPrice > 0){ + $price_range_query = " `price` >='".$minPrice."' "; + } + + $where_query[] = " AND ". $price_range_query; + } + + // ------------ + + return array( join(" ", $where_query), $bind_types, $bind_values); + } + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductSearchModel.php b/inc/Hura8/Components/Product/Model/ProductSearchModel.php new file mode 100644 index 0000000..4e8a30e --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductSearchModel.php @@ -0,0 +1,33 @@ + "tb_product.price", + "ranking" => "tb_product.ranking", + "status" => "tb_product.status", + "brand" => "tb_product.brand_id", + ]; + + private $fulltext_fields = [ + "category_keywords" => ["tb_product_category.title", ], + "product_keywords" => ["tb_product.sku", "tb_product.title", "tb_product.model", "tb_product.related_keywords"], + ]; + + + public function __construct() + { + parent::__construct( + "tb_product", + $this->fulltext_fields, + $this->filter_fields + ); + } + +} diff --git a/inc/Hura8/Components/Product/Model/ProductSpecGroupAttributeModel.php b/inc/Hura8/Components/Product/Model/ProductSpecGroupAttributeModel.php new file mode 100644 index 0000000..7cf1ae1 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductSpecGroupAttributeModel.php @@ -0,0 +1,65 @@ +db->runQuery( + "SELECT * FROM `".$this->tb_product_spec_group_record."` WHERE `group_id` = ? ORDER BY `ordering` ASC ", + ['d'], [$group_id] + ); + + return $this->db->fetchAll($query); + } + + + + protected function updateGroupAttributeCount($group_id) { + $this->db->runQuery("UPDATE `".$this->tb_product_spec_group."` SET + `attribute_count` = (SELECT COUNT(*) FROM `".$this->tb_product_spec_group_record."` WHERE `group_id` = ? ) + WHERE `id` = ? LIMIT 1 ", ['d', 'd' ], [ $group_id, $group_id ]); + } + + protected function buildQueryCondition(array $condition) + { + // TODO: Implement buildQueryCondition() method. + } + + protected function buildOrderByCondition($order_by) + { + // TODO: Implement buildOrderByCondition() method. + } + + + protected function _buildQueryConditionExtend(array $extend_filter_conditions) : ?array + { + return null; + } + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductSpecGroupModel.php b/inc/Hura8/Components/Product/Model/ProductSpecGroupModel.php new file mode 100644 index 0000000..5502134 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductSpecGroupModel.php @@ -0,0 +1,151 @@ +getSpecGroupAttribute($group_id); + } + + + protected function clearProductAttributes($product_id) { + $this->db->runQuery( + "DELETE FROM `".$this->tb_product_attribute."` WHERE `pro_id` = ? ", + [ 'd' ], [ $product_id ] + ); + } + + + public function clearProductSpecGroup($product_id) { + + $this->clearProductAttributes($product_id); + + $product_spec_group_id = $this->getProductSpecGroupId($product_id); + + // update + $this->db->runQuery( + "UPDATE `".$this->tb_product."` SET + `spec_group_id` = 0 + WHERE `id` = ? LIMIT 1 ", + [ 'd' ], [ $product_id ] + ); + + $this->updateSpecGroupProductCount($product_spec_group_id); + + return true; + } + + + public function setProductSpecGroup($product_id, $new_group_id) + { + $current_group_id = $this->getProductSpecGroupId($product_id); + + if($current_group_id != $new_group_id) { + // update + $this->db->runQuery( + "UPDATE `".$this->tb_product."` SET + `spec_group_id` = ? + WHERE `id` = ? LIMIT 1 ", + [ 'd', 'd' ], [ $new_group_id, $product_id ] + ); + + $this->updateSpecGroupProductCount($new_group_id); + + $this->clearProductAttributes($product_id); + + return true; + } + + return false; + } + + + protected function updateSpecGroupProductCount($group_id) { + $this->db->runQuery( + "UPDATE `".$this->tb_product_spec_group."` SET + `product_count` = (SELECT COUNT(*) FROM ".$this->tb_product." WHERE `spec_group_id` = ? ) + WHERE `id` = ? LIMIT 1 ", + [ 'd', 'd' ], [ $group_id, $group_id ] + ); + } + + + public function getProductSpecGroupInfo($product_id) + { + $product_spec_group_id = $this->getProductSpecGroupId($product_id); + + return ($product_spec_group_id) ? $this->getSpecGroupInfo($product_spec_group_id) : false ; + } + + + public function getProductSpecGroupId($product_id) + { + $query = $this->db->runQuery("SELECT `spec_group_id` FROM `".$this->tb_product."` WHERE `id` = ? LIMIT 1 ", ['d'], [$product_id]); + + if ($info = $this->db->fetchAssoc($query) ) { + return $info['spec_group_id'] ; + } + + return 0; + } + + + public function getSpecGroupInfo($group_id) + { + $query = $this->db->runQuery("SELECT * FROM `".$this->tb_product_spec_group."` WHERE `id` = ? ", ['d'], [$group_id]); + + return $this->db->fetchAssoc($query); + } + + + protected function buildQueryCondition(array $condition) + { + $catCondition = []; + + //Tim kiem theo tu khoa + if(isset($condition["q"]) && $condition["q"]){ + $catCondition[] = " AND `title` LIKE '%".$this->db->escape($condition["q"])."%' "; + } + + return join(" ", $catCondition); + } + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + $where_condition = ""; + $bind_types = []; + $bind_values = []; + + return [ + $where_condition, + $bind_types, + $bind_values + ]; + } + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductVariantModel.php b/inc/Hura8/Components/Product/Model/ProductVariantModel.php new file mode 100644 index 0000000..022a9c0 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductVariantModel.php @@ -0,0 +1,181 @@ +product_id = $product_id; + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function getProductVariantOption($product_id){ + $query = $this->db->runQuery("SELECT `variant_option` FROM `".$this->tb_product."` WHERE `id` = ? LIMIT 1", ['d'], [ $product_id ]) ; + + if($rs = $this->db->fetchAssoc($query)) { + return ($rs['variant_option']) ? \json_decode($rs['variant_option'], true) : null; + } + + return null; + } + + + //use a product's variant-option to create a sample, so next product can select without recreate from beginning + public function createVariantOptionSample($use_from_pro_id, $sample_title) { + + if( !$use_from_pro_id || strlen($sample_title) < 3 ) return false; + + $pro_variant_option = $this->getProductVariantOption($use_from_pro_id); + if(!$pro_variant_option) { + return false; + } + + $pro_variant_option_index = md5(\json_encode($pro_variant_option)); + + $check_duplicate = $this->db->runQuery( + "SELECT `id` FROM ".$this->tb_variant_option_sample." WHERE `variant_option_index` = ? LIMIT 1 ", + ['s'], [ $pro_variant_option_index ] + ); + + if($this->db->fetchAssoc($check_duplicate)) { + return false; + } + + // ok to save + $this->db->insert( + $this->tb_variant_option_sample, + [ + "title" => $sample_title, + "variant_option" => \json_encode($pro_variant_option), + "variant_option_index" => $pro_variant_option_index, + "create_time" => CURRENT_TIME, + ] + ); + + return true; + } + + public function getVariantOptionSample() { + $query = $this->db->runQuery("SELECT * FROM ".$this->tb_variant_option_sample." ORDER BY `id` DESC LIMIT 500 "); + + return $this->db->fetchAll($query); + } + + public function getProductVariantPriceRange(){ + $result = [ + "sale_price" => [ + "min" => 0, + "max" => 0, + ], + "market_price" => [ + "min" => 0, + "max" => 0, + ], + ]; + + $query = $this->db->runQuery( + " SELECT `sale_price`, `market_price`, `extend` FROM `".$this->tb_entity."` WHERE `product_id` = ? ", + ['d'], [$this->product_id] + ); + + foreach ( $this->db->fetchAll($query) as $info) { + + // find min + if($info["sale_price"] > 0 && ( $info["sale_price"] < $result["sale_price"]["min"] || $result["sale_price"]["min"] == 0 ) ) { + $result["sale_price"]["min"] = $info["sale_price"]; + } + + // find max + if($info["sale_price"] > 0 && $info["sale_price"] > $result["sale_price"]["max"] ) { + $result["sale_price"]["max"] = $info["sale_price"]; + } + + // market_price + $market_price = $info["market_price"]; + if($info['extend']) { + $extend = unserialize($info['extend']); + if(isset($extend['market_price'])) $market_price = clean_price($extend['market_price'], "vnd"); + } + + if($market_price > 0 && ( $market_price < $result["market_price"]["min"] || $result["market_price"]["min"] == 0 ) ) { + $result["market_price"]["min"] = $market_price; + } + + if($market_price > 0 && $market_price > $result["market_price"]["max"] ) { + $result["market_price"]["max"] = $market_price; + } + } + + return $result; + } + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + $where_condition = " AND `product_id` = ? "; + $bind_types = ["d"]; + $bind_values = [$this->product_id]; + + return [ + $where_condition, + $bind_types, + $bind_values + ]; + } + + protected function _buildQueryOrderBy($sort_by = "new") + { + return " `ordering` DESC, `id` DESC "; + } + + protected function formatItemInList(array $item_info): array + { + $info = $item_info; + + if($item_info['attribute']) $info['attribute'] = \json_decode($item_info['attribute'], true); + + return $info; + } + + protected function updateProductVariantCount() { + $this->db->runQuery( + "UPDATE ".$this->tb_product." SET + `config_count` = ( SELECT COUNT(*) AS total FROM `".$this->tb_entity."` WHERE `product_id` = ? AND `status` = 1 ) + WHERE `id` = ? ", + ['d', 'd'], [ $this->product_id, $this->product_id] + ) ; + } + + protected function formatItemInfo(array $item_info) : ?array + { + $info = $item_info; + + if($info['attribute']) $info['attribute'] = \json_decode($info['attribute'], true); + + return $info; + } + + +} diff --git a/inc/Hura8/Components/Province/AdminController/AProvinceController.php b/inc/Hura8/Components/Province/AdminController/AProvinceController.php new file mode 100644 index 0000000..d8589b7 --- /dev/null +++ b/inc/Hura8/Components/Province/AdminController/AProvinceController.php @@ -0,0 +1,10 @@ +objProvinceModel = new ProvinceModel(); + } + + + public function getProvinceName($id) { + return self::getCache("getProvinceName-".$id, function () use ($id) { + $info = $this->objProvinceModel->getProvinceInfo($id, 'name'); + return ($info) ? $info['name'] : ''; + }); + } + + public function getDistrictName($id) { + return self::getCache("getDistrictName-".$id, function () use ($id) { + $info = $this->objProvinceModel->getDistrictInfo($id, 'name'); + return ($info) ? $info['name'] : ''; + }); + } + + public function getWardName($id) { + return self::getCache("getWardName-".$id, function () use ($id) { + $info = $this->objProvinceModel->getWardInfo($id, 'name'); + return ($info) ? $info['name'] : ''; + }); + } + + + public function getProvinceList($field="*") { + return self::getCache("getProvinceList-", function () use ($field) { + return $this->objProvinceModel->getProvinceList($field); + //$pick_keys = ['id', 'api_id', 'name']; + + /*return array_map(function ($item) use($pick_keys){ + return pick_return_array_key($item, $pick_keys); + }, $province_list);*/ + }); + } + + public function getProvinceDistrictList($province_id, $field="*") { + + return self::getCache("getProvinceDistrictList-".$province_id, function () use ($province_id) { + return $this->objProvinceModel->getProvinceDistrictList($province_id); + //$pick_keys = ['id', 'api_id', 'name']; + + /*return array_map(function ($item) use($pick_keys){ + return pick_return_array_key($item, $pick_keys); + }, $province_list);*/ + }); + } + + public function getAllDistrictList() { + return self::getCache("getAllDistrictList", function (){ + return $this->objProvinceModel->getAllDistrictList(); + }); + } + + + public function getDistrictWardList($district_id, $field="*") { + return self::getCache("getDistrictWardList-".$district_id, function () use ($district_id) { + return $this->objProvinceModel->getDistrictWardList($district_id); + }); + } + + // these are kept for old time sake + /*public static $province_list = array( + "1" => "Hà Nội", + "2" => "TP HCM", + "5" => "Hải Phòng" , + "4" => "Đà Nẵng" , + "6" => "An Giang" , + "7" => "Bà Rịa-Vũng Tàu", + "13" => "Bình Dương" , + "15" => "Bình Phước" , + "16" => "Bình Thuận" , + "14" => "Bình Định" , + "8" => "Bạc Liêu", + "10" => "Bắc Giang" , + "9" => "Bắc Kạn", + "11" => "Bắc Ninh", + "12" => "Bến Tre", + "18" => "Cao Bằng", + "17" => "Cà Mau", + "3" => "Cần Thơ", + "24" => "Gia Lai", + "25" => "Hà Giang", + "26" => "Hà Nam", + "27" => "Hà Tĩnh", + "30" => "Hòa Bình", + "28" => "Hải Dương", + "29" => "Hậu Giang", + "31" => "Hưng Yên", + "32" => "Khánh Hòa", + "33" => "Kiên Giang", + "34" => "Kon Tum", + "35" => "Lai Châu", + "38" => "Lào Cai", + "36" => "Lâm Đồng", + "37" => "Lạng Sơn", + "39" => "Long An", + "40" => "Nam Định", + "41" => "Nghệ An", + "42" => "Ninh Bình", + "43" => "Ninh Thuận", + "44" => "Phú Thọ", + "45" => "Phú Yên", + "46" => "Quảng Bình", + "47" => "Quảng Nam", + "48" => "Quảng Ngãi", + "49" => "Quảng Ninh", + "50" => "Quảng Trị", + "51" => "Sóc Trăng", + "52" => "Sơn La", + "53" => "Tây Ninh", + "56" => "Thanh Hóa", + "54" => "Thái Bình", + "55" => "Thái Nguyên", + "57" => "Thừa Thiên-Huế", + "58" => "Tiền Giang", + "59" => "Trà Vinh", + "60" => "Tuyên Quang", + "61" => "Vĩnh Long", + "62" => "Vĩnh Phúc", + "63" => "Yên Bái", + "19" => "Đắk Lắk", + "22" => "Đồng Nai", + "23" => "Đồng Tháp", + "21" => "Điện Biên", + "20" => "Đăk Nông", + ); + + public static function getName($id) { + return (isset(static::$province_list[$id])) ? static::$province_list[$id] : ''; + }*/ + +} diff --git a/inc/Hura8/Components/Province/Model/ProvinceModel.php b/inc/Hura8/Components/Province/Model/ProvinceModel.php new file mode 100644 index 0000000..5d37f81 --- /dev/null +++ b/inc/Hura8/Components/Province/Model/ProvinceModel.php @@ -0,0 +1,73 @@ +db = get_db(); + } + + public function getProvinceInfo($id, $field="*") { + if(!$id) return null; + + $query = $this->db->runQuery("SELECT ".$field." FROM `".$this->tb_province."` WHERE `id` = ? LIMIT 1 ", ['d'], [ $id ]); + return $this->db->fetchAssoc($query); + } + + public function getDistrictInfo($id, $field="*") { + if(!$id) return null; + + $query = $this->db->runQuery("SELECT ".$field." FROM `".$this->tb_province_district."` WHERE `id` = ? LIMIT 1 ", ['d'], [ $id ]); + return $this->db->fetchAssoc($query); + } + + public function getWardInfo($id, $field="*") { + if(!$id) return null; + + $query = $this->db->runQuery("SELECT ".$field." FROM `".$this->tb_province_ward."` WHERE `id` = ? LIMIT 1 ", ['d'], [ $id ]); + return $this->db->fetchAssoc($query); + } + + public function getProvinceList($field="*") { + $query = $this->db->runQuery("SELECT ".$field." FROM `".$this->tb_province."` ORDER BY `id` ASC "); + return $this->db->fetchAll($query); + } + + public function getProvinceDistrictList($province_id, $field="*") { + $query = $this->db->runQuery( + "SELECT ".$field." FROM `".$this->tb_province_district."` WHERE `province_id` = ? ORDER BY `ordering` DESC ", + ['d'], [ $province_id ] + ); + + return $this->db->fetchAll($query); + } + + public function getAllDistrictList() { + $query = $this->db->runQuery("SELECT * FROM `".$this->tb_province_district."` WHERE 1 ORDER BY id ASC "); + return $this->db->fetchAll($query); + } + + public function getDistrictWardList($district_id, $field="*") { + $query = $this->db->runQuery( + "SELECT ".$field." FROM `".$this->tb_province_ward."` WHERE `district_id` = ? ORDER BY `ordering` DESC ", + ['d'], + [ $district_id ] + ); + + return $this->db->fetchAll($query); + } + +} diff --git a/inc/Hura8/Components/Staff/AdminController/ClientPermissionController.php b/inc/Hura8/Components/Staff/AdminController/ClientPermissionController.php new file mode 100644 index 0000000..30936aa --- /dev/null +++ b/inc/Hura8/Components/Staff/AdminController/ClientPermissionController.php @@ -0,0 +1,76 @@ +getClientEntityPermission_raw(); + }); + } + + protected function getClientEntityPermission_raw() { + + $system_file = ROOT_DIR. "/config/system/admin.entity.permission.php"; + $entity_group = include $system_file; + + $client_allowed_entities = Permission::getClientEntities(); + + $final_config = []; + foreach ($entity_group as $_group) { + + $settings = include ROOT_DIR. "/config/system/entity_permission/".$_group.".php"; + + $children_match = []; + foreach ($settings['children'] as $_entity => $_p) { + if(in_array($_entity, $client_allowed_entities)) { + $children_match[$_entity] = $_p; + } + } + + if(sizeof($children_match)) { + $final_config[$_group] = [ + 'title' => $settings['title'], + 'children' => $children_match, + ]; + } + } + + + return $final_config; + } + + + public function getClientMenu() { + return static::getCache("getClientMenu", function (){ + $menu_config_file = ROOT_DIR. "/config/client/admin/admin.menu.php"; + + $header_admin_config = []; + $menu_group = include $menu_config_file; + foreach ($menu_group as $_group) { + $content = include ROOT_DIR. "/config/client/admin/admin_menu/".$_group.".php"; + $enabled_menu_item = array_filter($content['menu'], function ($item){ return $item['enable'];}); + + if(sizeof($enabled_menu_item) > 0) { + $header_admin_config[$_group] = [ + 'enable' => $content['enable'], + 'name' => $content['name'], + 'url' => $content['url'], + 'menu' => $enabled_menu_item, + ]; + } + } + + return $header_admin_config; + }); + } + + +} diff --git a/inc/Hura8/Components/Staff/AdminController/StaffAdminController.php b/inc/Hura8/Components/Staff/AdminController/StaffAdminController.php new file mode 100644 index 0000000..314a63a --- /dev/null +++ b/inc/Hura8/Components/Staff/AdminController/StaffAdminController.php @@ -0,0 +1,72 @@ +objStaffAuthModel = new StaffAuthModel(); + $this->objStaffModel = new StaffModel(); + } + + public function getLoginListByIds(array $staff_ids) : array + { + return $this->objStaffAuthModel->getLoginListByIds($staff_ids); + } + + public function getList(array $conditions) : array + { + return $this->objStaffModel->getList($conditions); + } + + public function getInfo($id) : ?array + { + return $this->objStaffModel->getInfo($id); + } + + public function getEmptyInfo(array $additional_fields = []) : array + { + return $this->objStaffModel->getEmptyInfo($additional_fields); + } + + public function update($id, array $input_info) : AppResponse + { + // change password + if(isset($input_info['password']) && strlen($input_info['password']) > 5) { + $this->objStaffAuthModel->createOrUpdatePassword($id, $input_info['password']); + } + + return $this->objStaffModel->update($id, $input_info); + } + + public function create(array $input_info, $password = "") : AppResponse + { + $db_res = $this->objStaffModel->create($input_info); + + if($db_res->getStatus() == 'ok') { + + $new_id = $db_res->getData(); + + if(!$password) $password = IDGenerator::createStringId(6); + + $this->objStaffAuthModel->createOrUpdatePassword($new_id, $password); + + return new AppResponse('ok', '', ["id" => $new_id, "password" => $password]); + } + + return new AppResponse('error', 'Cannot create'); + } + + +} diff --git a/inc/Hura8/Components/Staff/AdminController/StaffAdminDepartmentController.php b/inc/Hura8/Components/Staff/AdminController/StaffAdminDepartmentController.php new file mode 100644 index 0000000..54ffdc5 --- /dev/null +++ b/inc/Hura8/Components/Staff/AdminController/StaffAdminDepartmentController.php @@ -0,0 +1,44 @@ +objStaffDepartmentModel = new StaffDepartmentModel(); + } + + public function getList(array $conditions) { + return $this->objStaffDepartmentModel->getList($conditions); + } + + public function getInfo($id) { + return self::getCache("getInfo-".$id, function () use ($id){ + return $this->objStaffDepartmentModel->getInfo($id); + }); + } + + public function getEmptyInfo(array $additional_fields = []) + { + return $this->objStaffDepartmentModel->getEmptyInfo($additional_fields); + } + + public function update($id, array $input_info) { + return $this->objStaffDepartmentModel->update($id, $input_info); + } + + public function create(array $input_info) { + return $this->objStaffDepartmentModel->create($input_info); + } + +} diff --git a/inc/Hura8/Components/Staff/AdminController/StaffDepartmentPermissionController.php b/inc/Hura8/Components/Staff/AdminController/StaffDepartmentPermissionController.php new file mode 100644 index 0000000..d54830d --- /dev/null +++ b/inc/Hura8/Components/Staff/AdminController/StaffDepartmentPermissionController.php @@ -0,0 +1,98 @@ +objStaffDepartmentPermissionModel = new StaffDepartmentPermissionModel(); + } + + + public function getDepartmentMenuPermissionSetting($department_id) { + + $current_permission = $this->getDepartmentMenuPermission($department_id); + + $user_menu_settings = []; + foreach ($this->getClientMenu() as $group_id => $group_info) { + + $rebuild_menu = []; + foreach ($group_info['menu'] as $index => $menu) { + $rebuild_menu[$index] = $menu; + + $rebuild_menu[$index]['is_user_permitted'] = (in_array($menu['id'], $current_permission)) ? 1 : 0; + } + + $user_menu_settings[$group_id] = $group_info; + $user_menu_settings[$group_id]['menu'] = $rebuild_menu; + } + + return $user_menu_settings; + } + + + public function saveDepartmentMenuPermission($department_id, array $new_permission) { + $this->objStaffDepartmentPermissionModel->saveDepartmentMenuPermission($department_id, $new_permission); + } + + + public function getDepartmentMenuPermission($department_id) { + return $this->objStaffDepartmentPermissionModel->getDepartmentMenuPermission($department_id); + } + + + public function getDepartmentEntityPermissionSetting($department_id) { + + $current_permission = $this->getDepartmentEntityPermission($department_id); + + $_settings = []; + foreach ($this->getClientEntityPermission() as $group_id => $group_info) { + + $rebuild_children = []; + foreach ($group_info['children'] as $entity => $info) { + + $actions = []; + foreach ($info['action'] as $action_key => $action_title) { + $actions[] = [ + 'action' => $action_key, + 'title' => $action_title, + 'is_user_permitted' => (isset($current_permission[$group_id]) && isset($current_permission[$group_id][$entity]) && isset($current_permission[$group_id][$entity][$action_key])) ? $current_permission[$group_id][$entity][$action_key] : false , + ]; + } + + $rebuild_children[$entity] = [ + 'title' => $info['title'], + 'action_list' => $actions, + ]; + + } + + $_settings[$group_id] = $group_info; + $_settings[$group_id]['children'] = $rebuild_children; + } + + return $_settings; + } + + + public function saveDepartmentEntityPermission($department_id, array $new_permission) { + $this->objStaffDepartmentPermissionModel->saveDepartmentEntityPermission( + $department_id, $new_permission + ); + } + + + public function getDepartmentEntityPermission($department_id) { + return static::getCache("getDepartmentEntityPermission-".$department_id, function () use ($department_id){ + return $this->objStaffDepartmentPermissionModel->getDepartmentEntityPermission($department_id); + }) ; + } + +} diff --git a/inc/Hura8/Components/Staff/AdminController/StaffLogController.php b/inc/Hura8/Components/Staff/AdminController/StaffLogController.php new file mode 100644 index 0000000..f1f7e80 --- /dev/null +++ b/inc/Hura8/Components/Staff/AdminController/StaffLogController.php @@ -0,0 +1,27 @@ +objStaffLogModel = new StaffLogModel(); + } + + public function getList(array $conditions) { + return $this->objStaffLogModel->getList($conditions); + } + + public function getTotal(array $conditions) { + return $this->objStaffLogModel->getTotal($conditions); + } + + public function create(array $input_info) { + + } +} diff --git a/inc/Hura8/Components/Staff/AdminController/StaffPermissionController.php b/inc/Hura8/Components/Staff/AdminController/StaffPermissionController.php new file mode 100644 index 0000000..d87f2ca --- /dev/null +++ b/inc/Hura8/Components/Staff/AdminController/StaffPermissionController.php @@ -0,0 +1,180 @@ +objStaffPermissionModel = new StaffPermissionModel(); + } + + /** + * @param $entity_group + * @param $entity + * @param string $action valid values in PermissionType:: + * @return bool + */ + public function checkEntityActionPermission($is_super_user, $entity_group, $entity, $action ) : bool { + + // super is allowed by default + if($is_super_user) { + return true; + } + + $current_permission = $this->getCurrentUserEntityPermission(); + + if ( + isset($current_permission[$entity_group]) && + isset($current_permission[$entity_group][$entity]) && + isset($current_permission[$entity_group][$entity][$action]) + ) { + return $current_permission[$entity_group][$entity][$action] ; + } + + return false; + } + + + public function getUserProductMenuList() { + + return self::getCache("getUserProductMenuList", function (){ + $menu_list = include CONFIG_DIR. '/client/admin/product_menu.php'; + + $final_list = []; + foreach (array_filter($menu_list, function ($item){ return $item['enabled'];}) as $key => $value) { + + // check if the current staff can see + // todo + // ... + + $final_list[$key] = $value; + } + + return $final_list; + }); + + } + + + public function saveUserMenuPermission($admin_id, array $new_permission) { + $this->objStaffPermissionModel->saveUserMenuPermission($admin_id, $new_permission); + } + + + public function saveUserEntityPermission($admin_id, array $new_permission) { + $this->objStaffPermissionModel->saveUserEntityPermission($admin_id, $new_permission); + } + + /* + * get menu for currently logged-in user + */ + public function getUserMenu($is_super_user) { + + // super can see all + if($is_super_user) { + return $this->getClientMenu(); + } + + $current_permission = $this->getCurrentUserMenuPermission(); + + $user_menu_settings = []; + foreach ($this->getClientMenu() as $group_id => $group_info) { + + $rebuild_menu = []; + foreach ($group_info['menu'] as $index => $menu) { + if(!in_array($menu['id'], $current_permission)) continue; + + $rebuild_menu[$index] = $menu; + } + + if(sizeof($rebuild_menu)) { + $user_menu_settings[$group_id] = $group_info; + $user_menu_settings[$group_id]['menu'] = $rebuild_menu; + } + } + + return $user_menu_settings; + } + + + public function getCurrentUserMenuPermission() { + $current_staff_id = StaffLoginController::getLoggedInStaffId(); + return $this->objStaffPermissionModel->getUserMenuPermission($current_staff_id); + } + + + public function getCurrentUserEntityPermission() { + $current_staff_id = StaffLoginController::getLoggedInStaffId(); + return $this->objStaffPermissionModel->getUserEntityPermission($current_staff_id); + } + + + public function getUserMenuPermissionSetting($admin_id) { + + $current_permission = $this->objStaffPermissionModel->getUserMenuPermission($admin_id); + + $user_menu_settings = []; + foreach ($this->getClientMenu() as $group_id => $group_info) { + + $rebuild_menu = []; + foreach ($group_info['menu'] as $index => $menu) { + $rebuild_menu[$index] = $menu; + + $rebuild_menu[$index]['is_user_permitted'] = (in_array($menu['id'], $current_permission)) ? 1 : 0; + } + + $user_menu_settings[$group_id] = $group_info; + $user_menu_settings[$group_id]['menu'] = $rebuild_menu; + } + + return $user_menu_settings; + } + + + public function getUserEntityPermissionSetting($admin_id) { + + $current_permission = $this->objStaffPermissionModel->getUserEntityPermission($admin_id); + + $_settings = []; + + foreach ($this->getClientEntityPermission() as $group_id => $group_info) { + + $rebuild_children = []; + foreach ($group_info['children'] as $entity => $info) { + + $actions = []; + foreach ($info['action'] as $action_key => $action_title) { + $actions[] = [ + 'action' => $action_key, + 'title' => $action_title, + 'is_user_permitted' => ( + isset($current_permission[$group_id]) && + isset($current_permission[$group_id][$entity]) && + isset($current_permission[$group_id][$entity][$action_key]) + ) ? $current_permission[$group_id][$entity][$action_key] : false , + ]; + } + + $rebuild_children[$entity] = [ + 'title' => $info['title'], + 'action_list' => $actions, + ]; + + } + + $_settings[$group_id] = $group_info; + $_settings[$group_id]['children'] = $rebuild_children; + } + + return $_settings; + } + +} diff --git a/inc/Hura8/Components/Staff/Model/StaffAuthModel.php b/inc/Hura8/Components/Staff/Model/StaffAuthModel.php new file mode 100644 index 0000000..8430a6d --- /dev/null +++ b/inc/Hura8/Components/Staff/Model/StaffAuthModel.php @@ -0,0 +1,64 @@ +tb_staff_login, $this->tb_staff_access_code); + } + + + public function getLoginListByIds(array $staff_ids) { + if(!sizeof($staff_ids)) { + return []; + } + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($staff_ids, 'int'); + + $bind_values = $staff_ids; + + $query = $this->db->runQuery( + "SELECT `user_id`, `last_login_time`, `last_login_ip`, `last_login_device`, `last_login_browser` + FROM ".$this->tb_staff_login." + WHERE `user_id` IN (".$parameterized_ids.") ", + $bind_types, + $bind_values + ); + + $item_list = []; + foreach ($this->db->fetchAll($query) as $item) { + $item_list[$item['user_id']] = $item; + } + + return $item_list; + } + + + public function getLoginLog(array $conditions = []) { + $bind_types = []; + $bind_values = []; + + $query = $this->db->runQuery( + "SELECT * FROM ".$this->tb_staff_login_log." WHERE 1 ORDER BY `id` DESC LIMIT 100 ", + $bind_types, + $bind_values + ); + + return $this->db->fetchAll($query) ; + } + + + +} diff --git a/inc/Hura8/Components/Staff/Model/StaffDepartmentModel.php b/inc/Hura8/Components/Staff/Model/StaffDepartmentModel.php new file mode 100644 index 0000000..1e15000 --- /dev/null +++ b/inc/Hura8/Components/Staff/Model/StaffDepartmentModel.php @@ -0,0 +1,40 @@ +objStaffDepartmentModel = new StaffDepartmentModel(); + } + + + public function getDepartmentMenuPermission($department_id) { + $_info = $this->objStaffDepartmentModel->getInfo($department_id); + + return ($_info['menu_permission']) ? \json_decode($_info['menu_permission'], true) : []; + } + + + + public function getDepartmentEntityPermission($department_id) { + $_info = $this->objStaffDepartmentModel->getInfo($department_id); + + return ($_info['entity_permission']) ? \json_decode($_info['entity_permission'], true) : []; + } + +} diff --git a/inc/Hura8/Components/Staff/Model/StaffLogModel.php b/inc/Hura8/Components/Staff/Model/StaffLogModel.php new file mode 100644 index 0000000..087b6a8 --- /dev/null +++ b/inc/Hura8/Components/Staff/Model/StaffLogModel.php @@ -0,0 +1,38 @@ +tb_staff = $this->tb_entity; + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function getInfoByEmail($email) : ?array + { + $query = $this->db->runQuery("SELECT * FROM `".$this->tb_entity."` WHERE `email` = ? LIMIT 1 ", ['s'], [$email]) ; + if( $item_info = $this->db->fetchAssoc($query)){ + return $this->formatItemInfo($item_info); + } + + return null; + } + + + protected function _buildQueryConditionExtend(array $condition) : ?array + { + /*$condition = array( + "letter" => "", + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + if(isset($filter_condition["letter"]) && strlen($filter_condition["letter"]) == 1){ + $catCondition[] = " AND `letter` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["letter"]; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + +} diff --git a/inc/Hura8/Components/Staff/Model/StaffPermissionModel.php b/inc/Hura8/Components/Staff/Model/StaffPermissionModel.php new file mode 100644 index 0000000..f9f9049 --- /dev/null +++ b/inc/Hura8/Components/Staff/Model/StaffPermissionModel.php @@ -0,0 +1,67 @@ +objStaffModel = new StaffModel(); + } + + + public function getUserEntityPermission($admin_id) { + + /*return [ + // entity group => entity + 'product' => [ + 'item' => [ + 'view' => true, + 'create' => false, + 'delete' => true, + 'update' => false, + ], + ], + + 'article' => [ + 'item' => [ + 'view' => true, + 'create' => false, + 'delete' => false, + 'update' => true, + ], + ] + ];*/ + + $admin_info = $this->objStaffModel->getInfo($admin_id); + + if($admin_info['entity_permission']) { + $entity_permission = \json_decode($admin_info['entity_permission'], true); + + if(sizeof($entity_permission) > 0) { + return $entity_permission; + } + } + + // else use department + if($admin_info['department']) { + $objStaffDepartmentPermissionModel = new StaffDepartmentPermissionModel(); + $department_entity_permission = $objStaffDepartmentPermissionModel->getDepartmentEntityPermission($admin_info['department']); + + return $department_entity_permission; + } + + return []; + } + + + public function getUserMenuPermission($admin_id) { + $admin_info = $this->objStaffModel->getInfo($admin_id); + return ($admin_info['menu_permission']) ? \json_decode($admin_info['menu_permission'], true) : []; + } + +} diff --git a/inc/Hura8/Components/Template/AdminController/ATemplateController.php b/inc/Hura8/Components/Template/AdminController/ATemplateController.php new file mode 100644 index 0000000..06d12a8 --- /dev/null +++ b/inc/Hura8/Components/Template/AdminController/ATemplateController.php @@ -0,0 +1,216 @@ +objTemplateModel = new TemplateModel($template_set); + $this->template_set = $template_set; + + $this->checkAndCreateDirectory(); + } + + public function checkFileExist($file_name, $f_type ) { + return $this->objTemplateModel->checkFileExist($file_name, $f_type); + } + + public function getInfo($id) { + return $this->objTemplateModel->getInfo($id); + } + + public function getEmptyInfo($addition_field_value = []) { + return $this->objTemplateModel->getEmptyInfo($addition_field_value); + } + + public function getModuleList() { + return $this->objTemplateModel->getModuleList(); + } + + + /** + * @description full tpl file name has this format: language_layout_base-name + * language is omitted if language = 'vnd' + * layout is omitted if layout = 'pc' + Example: + * for vn/pc: + - index + - product_detail + * for vn/mobile: + - mobile_index + - mobile_product_detail + * for en/pc: + - en_index + - en_product_detail + * for en/mobile: + - en_mobile_index + - en_mobile_product_detail + * @description contruct a full tpl file name to store in folder filesystem + * @param string $language vn or en + * @param string $layout pc or mobile + * @param string $base_name should not contain reserved keywords used for language (i.e en) & layout (i.e mobile) + * @return string + * @test + debug_var(\Hura8\Admin\ATemplate::buildFullTplFileName("product_detail", "vn", "pc")); + debug_var(\Hura8\Admin\ATemplate::buildFullTplFileName("product_detail", "vn", "mobile")); + debug_var(\Hura8\Admin\ATemplate::buildFullTplFileName("product_detail", "en", "pc")); + debug_var(\Hura8\Admin\ATemplate::buildFullTplFileName("product_detail", "en", "mobile")); + */ + public static function buildFullTplFileName(string $base_name, string $language=DEFAULT_LANGUAGE, string $layout='pc') { + if($language == DEFAULT_LANGUAGE) { + return ($layout == 'pc') ? $base_name : join("_", [$layout, $base_name]); + } + + return ($layout == 'pc') ? join("_", [$language, $base_name]) : join("_", [$language, $layout, $base_name]); + } + + /** + * @description extract $language, $layout, $base_name from $full_tpl_file + * @param string $full_tpl_file + * @return array + * @test + debug_var(\Hura8\Admin\ATemplate::extractFullTplFileName("product_detail")); + debug_var(\Hura8\Admin\ATemplate::extractFullTplFileName("mobile_product_detail")); + debug_var(\Hura8\Admin\ATemplate::extractFullTplFileName("en_product_detail")); + debug_var(\Hura8\Admin\ATemplate::extractFullTplFileName("en_mobile_product_detail")); + */ + public static function extractFullTplFileName(string $full_tpl_file) { + $language = DEFAULT_LANGUAGE; + $layout = "pc"; + + // check for different language + if(substr($full_tpl_file, 0, 3) == "en_") { + $language = "en"; + } + + // check for different layout + $clean_file = str_replace($language."_", "", $full_tpl_file); + if(substr($clean_file, 0, 7) == "mobile_") { + $layout = "mobile"; + } + + $base_name = str_replace($layout."_", "", $clean_file); + + return [ $language, $layout, $base_name ]; + } + + protected function checkAndCreateDirectory() { + // check and create directory + if (!is_dir($this->web_template_path)) { + mkdir($this->web_template_path, 0755, true); + } + + if (!is_dir($this->assets_template_path)) { + mkdir($this->assets_template_path, 0755, true); + mkdir(join(DIRECTORY_SEPARATOR, [$this->assets_template_path, 'script']), 0755, true); + mkdir(join(DIRECTORY_SEPARATOR, [$this->assets_template_path, 'images']), 0755, true); + } + } + + public function getFiles(array $conditions = []) { + + $conditions['numPerPage'] = 2000; + + return $this->objTemplateModel->getList($conditions); + } + + + public function getVersionHistory($module, $tpl_file) { + return $this->objTemplateModel->getVersionHistory($module, $tpl_file); + } + + public function getVersionContent($module, $tpl_file, $version) { + return $this->objTemplateModel->getVersionContent($module, $tpl_file, $version); + + } + + public function getFileContent($tpl_file, $folder, $is_setting = false) { + $ext = strrchr($tpl_file, "."); + if ($ext) $tpl_file = str_replace($ext, "", $tpl_file); + + switch ($ext) { + // css or js files + case ".script": + $full_file_path = join(DIRECTORY_SEPARATOR, [$this->assets_template_path, "script", $tpl_file]); + $content = (@file_exists($full_file_path)) ? @file_get_contents($full_file_path) : ''; + $real_ext = strrchr($tpl_file, "."); + $editor_mode = ($real_ext == ".js") ? 'text/javascript' : 'text/css'; + + return "
+ + +
+ "; + + + case ".image": + + $full_file_path = join(DIRECTORY_SEPARATOR, [$this->assets_template_path, "images", $tpl_file]); + return "
+ +
"; + + + default; + + $full_file_path = ($folder != 'index') ? + join(DIRECTORY_SEPARATOR, [$this->web_template_path, $folder, $tpl_file.".html"]) : + join(DIRECTORY_SEPARATOR, [$this->web_template_path, $tpl_file.".html"]) ; + + $content = (@file_exists($full_file_path)) ? @file_get_contents($full_file_path) : ''; + + return " + +
+ +
+ + "; + + } + } + +} diff --git a/inc/Hura8/Components/Template/AdminController/ATemplateSetController.php b/inc/Hura8/Components/Template/AdminController/ATemplateSetController.php new file mode 100644 index 0000000..509b532 --- /dev/null +++ b/inc/Hura8/Components/Template/AdminController/ATemplateSetController.php @@ -0,0 +1,46 @@ +objTemplateSetModel = new TemplateSetModel(); + } + + + // get empty/default item for form + public function getEmptyInfo($addition_field_value = []) { + return $this->objTemplateSetModel->getEmptyInfo($addition_field_value); + } + + public function checkSetExist($set_name) + { + return $this->objTemplateSetModel->getInfoBySet($set_name); + } + + public function getActivatedSet() { + return $this->objTemplateSetModel->getActivatedSet(); + } + + public function getList(array $condition) + { + return $this->objTemplateSetModel->getList($condition); + } + + public function getInfo($id): ?array + { + return $this->objTemplateSetModel->getInfo($id); + } + +} diff --git a/inc/Hura8/Components/Template/Controller/TemplateController.php b/inc/Hura8/Components/Template/Controller/TemplateController.php new file mode 100644 index 0000000..a9ef656 --- /dev/null +++ b/inc/Hura8/Components/Template/Controller/TemplateController.php @@ -0,0 +1,167 @@ +template_set = $template_set; + $this->web_template_path = self::TEMPLATE_DIR . $template_set; + $this->assets_template_path = self::TEMPLATE_ASSETS_DIR . $template_set; + $this->assets_public_path = join('/', ['', 'static', 'assets', $template_set]) ; + } + + + /** + * @param string $language_prefix + * @param string $layout + * @param $routing array + * @return string + */ + public static function getPublicTemplateFile($language_prefix="", $layout="pc", array $routing=[]): string{ + $tpl_file = str_replace('-', '_', $routing['view']). '.html'; + if($layout != 'pc') $tpl_file = $layout.'_'.$tpl_file; + + if($language_prefix) $tpl_file = $language_prefix."_".$tpl_file; + + return join( DIRECTORY_SEPARATOR, [ + $routing['module'], + $tpl_file + ]); + } + + + /** + * @param string $language_prefix + * @param string $layout + * @return string + */ + public static function getSiteThemeFile($language_prefix="", $layout = 'pc'): string + { + $tpl_file = "index.html"; + if($layout != 'pc') $tpl_file = $layout.'_'.$tpl_file; + + if($language_prefix) return $language_prefix."_".$tpl_file; + + return $tpl_file; + } + + + public static function flushCache() { + try { + + $cache = new File([ + 'cache_dir' => self::$cache_dir + ]); + + $cache->flush(); + + }catch (\Exception $exception) { + + } + } + + /* + * 2 ways to render a html template + * 1. Use $html_to_parse, which requires no dependencies + * Example: + * Template::parse(null, 'Age = {{age}}', ['age' => 21], ''); + * + * 2. Use $template_file_path, which requires dependency $path + * Template::parse(Template::$setting_template_path, null, ['age' => 21], 'email/test'); + * + * */ + public static function parse($path = null, $html_to_parse = null, array $data = [], $template_file_path = '') { + + if(!$html_to_parse && !$template_file_path) { + return 'Nothing to parse'; + } + + //output to html + Liquid::set('INCLUDE_SUFFIX', 'html'); + Liquid::set('INCLUDE_PREFIX', ''); + //Liquid::set('INCLUDE_ALLOW_EXT', true); + Liquid::set('ESCAPE_BY_DEFAULT', false); + + $enable_cache = false; // default = true, turn this on-off to disable cache while working on local mode + //$enable_cache = true; + + //catch exception and print friendly notice + try { + + $objLiquidTemplate = new LiquidTemplate( $path ); + $objLiquidTemplate->registerFilter( TemplateFilter::class ); + if($enable_cache) { + $objLiquidTemplate->setCache(new File([ + 'cache_dir' => self::$cache_dir + ])); + } + + if($html_to_parse) { + $objLiquidTemplate->parse($html_to_parse); + }elseif ($template_file_path) { + $objLiquidTemplate->parseFile($template_file_path); + } + + return $objLiquidTemplate->render($data); + + } catch (\Exception $e) { + $result = []; + do { + //printf("%s:%d %s (%d) [%s]\n", $e->getFile(), $e->getLine(), $e->getMessage(), $e->getCode(), get_class($e)); + //echo $e->getTraceAsString(); + //$code = $e->getTrace()[0]['args'][0]; + //if(is_array($code)) $code = serialize($code); + $result[] = sprintf( + " +Lỗi code trong file template html:
+- Chi tiết lỗi: %s
+- File template: %s
+- Cache Dir: %s
+- Hướng dẫn xử lý: Tách từng phần html để kiểm tra và nhấn F5 mỗi lần. Nếu không xuất hiện thông báo này nghĩa là phần đó không tạo lỗi +", + $e->getMessage(), + substr($template_file_path, strrpos($template_file_path, DIRECTORY_SEPARATOR) + 1 ), + static::$cache_dir + ); + + } while($e = $e->getPrevious()); + + return join(" - ", $result); + } + } + + + public function getWebTemplatePath() { + return $this->web_template_path; + } + + public function getWebPublicAssetsUrl() { + return $this->assets_public_path; + } + + public function getWebTemplateAssetsPath() { + return $this->assets_template_path; + } + +} diff --git a/inc/Hura8/Components/Template/Controller/TemplateFilter.php b/inc/Hura8/Components/Template/Controller/TemplateFilter.php new file mode 100644 index 0000000..b164647 --- /dev/null +++ b/inc/Hura8/Components/Template/Controller/TemplateFilter.php @@ -0,0 +1,262 @@ + '', 'value' => ] from [key1 => value1, key2=>value2, ...] + * + * @param array $key_values [key1 => value1, key2=>value2] + * + * @return array [['key' => 'key1', 'value' => value1], ['key' => 'key2', 'value' => value2]] + */ + public static function to_array(array $key_values) { + $result = []; + foreach ($key_values as $key => $value) { + $result[] = [ + 'key' => $key, + 'value' => $value, + ]; + } + + return $result; + } + + + /** + * split a s by line to create array + * + * @param string $txt + * + * @return array + */ + public static function get_line($txt) { + + if(is_array($txt)) { + return $txt; + } + + $txt = trim($txt); + if( ! $txt ) return []; + + return preg_split("/\n/", $txt); + } + + /** + * Implement strlen + * + * @param string $str + * + * @return int + */ + public static function length($str) { + return strlen(trim($str)); + } + + + /** + * Make number easier to read: 1000 -> 1.000 + * + * @param string $number + * + * @return string + */ + public static function format_number($number) { + if(!$number) return ''; + $number = floatval($number); + + $number = number_format($number, 0, ",", "."); //Vietnamese format with decimals by a coma + + return $number; + } + + public static function format_price($p_price, $currency = ''){ + if(!$p_price) return ''; + if(!$currency) $currency = (defined("DEFAULT_CURRENCY")) ? DEFAULT_CURRENCY : "vnd"; + //if(is_string($p_price)) return 0; + if($currency == 'usd') { + return number_format($p_price,2,".",","); + }else { + return number_format($p_price,0,",","."); + } + } + + public static function global_asset_url($file_name = '') + { + return GLOBAL_ASSETS_PATH . $file_name; + } + + /** + * + * Description: get the shop's full asset url for template's images/js/css + * + * //Returns the URL of a file in the "assets" folder of a theme. + // {{ 'shop.css' | asset_url : 'arg1', 'arg2' ...}} -> //cdn.shopify.com/s/files/1/0087/0462/t/394/assets/shop.css?28253 + * + * @param string $file_name + * + * @return string + */ + public static function asset_url($file_name = '') + { + if( !$file_name ) return ''; + + $file_ext = strtolower(strrchr($file_name, ".")); + + // script tags + if(in_array($file_ext, ['.js', '.css'])) return TEMPLATE_ASSET_SCRIPT . $file_name; + + // default image + return TEMPLATE_ASSET_IMAGE . $file_name; + } + + + /** + * + * Description: construct a full html tag for images/js/css file + * + * @param string $file_path domain.com/static/style.css?v=3.1.1 + * + * @return string + */ + public static function script_tag($file_path) { + if( ! $file_path ) return ''; + + //check for ? + if(strpos($file_path, "?") !== false) { + $file_ext = str_replace(strrchr($file_path, "?"), "", $file_path); + $file_ext = strtolower(strrchr($file_ext, ".")); + } else { + $file_ext = strtolower(strrchr($file_path, ".")); + } + + $tag_config = [ + ".css" => "", + ".js" => "", + ".jpg" => "\"n\"/", + ".jpeg" => "\"\"/", + ".gif" => "\"\"/", + ".png" => "\"\"/", + ]; + + return (isset($tag_config[$file_ext])) ? $tag_config[$file_ext] : ''; + } + + /** + * {{ product_info.main_image | img_url: '300x300' }} => https://cdn.shopify.com/s/files/1/1183/1048/products/boat-shoes_300x300.jpeg?1459175177 + * @param string $full_path + * @param string $modifier + * $modifier: + * - must be in format: NumberxNumber or Numberx where Number must within 10 -> 9999 + * - or be one of these: small | medium | large + * @return string + */ + public static function img_url($full_path, $modifier) + { + $clean_modifier = ($modifier) ? trim($modifier) : ""; + + // verify $modifier + // must be in format: NumberxNumber or Numberx where Number must within 10 -> 9999 + if($clean_modifier + && !preg_match("/^[0-9]{2,4}x([0-9]{2,4})?$/i", $clean_modifier) + && !in_array($clean_modifier, ["small", "medium", "large"]) + ) { + $clean_modifier = ""; + } + + // return if no valid modifier + if( ! $clean_modifier ) { + return $full_path; + } + + $last_dot_position = strrpos($full_path, "."); + if( ! $last_dot_position ) return $full_path . $clean_modifier; + + return join("", [ + substr($full_path, 0, $last_dot_position), + "_", + $clean_modifier, + substr($full_path, $last_dot_position) + ]); + } + + /** + * //Returns the URL of a file in the Files page of the admin. + //{{ 'size-chart.pdf' | file_url }} -> //cdn.shopify.com/s/files/1/0087/0462/files/size-chart.pdf?28261 + * + * @param string $input + * @param string $string + * + * @return string + */ + public static function file_url($input, $string) + { + return strtoupper($input) . " = " . $string; + } + + /** + * //Returns the asset URL of an image in the Files page of the admin. file_img_url accepts an image size parameter. + //{{ 'logo.png' | file_img_url: '1024x768' }} -> //cdn.shopify.com/s/files/1/0246/0527/files/logo_1024x768.png?42 + * + * @param string $input + * @param string $string + * + * @return string + */ + public static function file_img_url($input, $string) + { + return ''; + } + + + /** + * Show all content of a variable, useful for template development + * + * @param string + * + * @return string + */ + public static function print_r($input) + { + @ob_start(); + print_r($input); + $content = ob_get_contents(); + @ob_end_clean(); + + return join("\r", ['']) ; + } + + /** + * Show all content of a variable, useful for template development + * + * @param string + * + * @return string + */ + public static function show_var($input) + { + @ob_start(); + print_r($input); + $content = ob_get_contents(); + @ob_end_clean(); + + return join("\r", ['']) ; + } +} diff --git a/inc/Hura8/Components/Template/Model/TemplateModel.php b/inc/Hura8/Components/Template/Model/TemplateModel.php new file mode 100644 index 0000000..da07a12 --- /dev/null +++ b/inc/Hura8/Components/Template/Model/TemplateModel.php @@ -0,0 +1,132 @@ +template_set = $template_set; + $this->tb_template = $this->tb_entity; + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function getModuleList() { + $query = $this->db->runQuery( + " select distinct `module` from `".$this->tb_template."` where `set_name` = ? order by `module` asc ", + [ 's' ], + [ $this->template_set ] + ) ; + + $item_list = []; + foreach ($this->db->fetchAll($query) as $item) { + if($item['module']) $item_list[] = $item['module']; + } + + return $item_list; + } + + + public function checkFileExist($file_name, $f_type ) { + $query = $this->db->runQuery( + "SELECT * FROM ".$this->tb_template." WHERE `file_name` = ? AND `file_type` = ? AND `set_name` = ? LIMIT 1 ", + [ 's', 's' , 's' ], + [ $file_name, $f_type, $this->template_set ] + ); + + return $this->db->fetchAssoc($query); + } + + + public function getVersionHistory($module, $tpl_file) { + + if(!$tpl_file) return []; + + $history_tpl_file = $this->historyFile($module, $tpl_file); + + $query = $this->db->runQuery( + "SELECT tpl_version, last_update, last_update_by FROM ".$this->tb_template_history." + WHERE `template` = ? AND `set_name` = ? ORDER BY `tpl_version` DESC LIMIT 30 ", + [ 's', 's' ], + [ $history_tpl_file, $this->template_set ] + ); + + return $this->db->fetchAll($query); + } + + + public function getVersionContent($module, $tpl_file, $version) { + + $history_tpl_file = $this->historyFile($module, $tpl_file); + + $query = $this->db->runQuery( + "SELECT `content` FROM ".$this->tb_template_history." + WHERE `template`= ? AND `set_name` = ? AND `tpl_version` = ? + LIMIT 1 ", + [ 's', 's', 'd' ], + [ $history_tpl_file, $this->template_set, $version ] + ); + + if ($rs = $this->db->fetchAssoc($query)) { + return $rs['content']; + } + + return ''; + } + + protected function historyFile($module, $tpl_file) { + return join("/", [$module, $tpl_file]); + } + + + protected function _buildQueryConditionExtend(array $conditions) : ?array + { + + /*$conditions = [ + 'file_folder' => '', + 'not_image' => true, + ];*/ + + $where_clause = [ " AND `set_name` = ? "]; + $bind_types = ['s']; + $bind_values = [$this->template_set]; + + if(isset($conditions['file_folder']) && in_array($conditions['file_folder'], ['layout', 'images', 'script']) ) { + $where_clause[] = " AND `file_folder` = ? "; + $bind_types[] = 's'; + $bind_values[] = $conditions['file_folder']; + } + + if(isset($conditions['not_image']) && $conditions['not_image']) { + $where_clause[] = " AND `file_folder` != 'images' "; + } + + return [ + join(" ", $where_clause), + $bind_types, + $bind_values + ]; + } + + +} diff --git a/inc/Hura8/Components/Template/Model/TemplateSetModel.php b/inc/Hura8/Components/Template/Model/TemplateSetModel.php new file mode 100644 index 0000000..f7733ac --- /dev/null +++ b/inc/Hura8/Components/Template/Model/TemplateSetModel.php @@ -0,0 +1,64 @@ +db->runQuery("SELECT * FROM `". $this->tb_entity ."` WHERE `folder_name` = ? LIMIT 1 ", ['s'], [$set_name]); + if($item_info = $this->db->fetchAssoc($query)){ + return $item_info; + } + + return false; + } + + + public function getActivatedSet() { + $query = $this->db->runQuery("SELECT `folder_name` FROM `". $this->tb_entity ."` WHERE `is_activated` = 1 LIMIT 1 "); + if($item_info = $this->db->fetchAssoc($query)){ + return $item_info['folder_name']; + } + + return 'default'; + } + + + protected function _buildQueryConditionExtend(array $condition) : ?array + { + $where_clause = ""; + $bind_types = []; + $bind_values = []; + + return [ + $where_clause, + $bind_types, + $bind_values + ]; + } + + + +} diff --git a/inc/Hura8/Components/User/AdminController/ANewsletterController.php b/inc/Hura8/Components/User/AdminController/ANewsletterController.php new file mode 100644 index 0000000..d208f8f --- /dev/null +++ b/inc/Hura8/Components/User/AdminController/ANewsletterController.php @@ -0,0 +1,27 @@ +objNewsletterModel = new NewsletterModel(); + parent::__construct($this->objNewsletterModel); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // TODO: Implement deleteFileBeforeDeleteItem() method. + return true; + } + + +} diff --git a/inc/Hura8/Components/User/AdminController/AUserCommentController.php b/inc/Hura8/Components/User/AdminController/AUserCommentController.php new file mode 100644 index 0000000..5f854bf --- /dev/null +++ b/inc/Hura8/Components/User/AdminController/AUserCommentController.php @@ -0,0 +1,27 @@ +objUserCommentModel = new UserCommentModel(); + parent::__construct($this->objUserCommentModel); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // TODO: Implement deleteFileBeforeDeleteItem() method. + return true; + } + +} diff --git a/inc/Hura8/Components/User/AdminController/AUserContactController.php b/inc/Hura8/Components/User/AdminController/AUserContactController.php new file mode 100644 index 0000000..05ea5b2 --- /dev/null +++ b/inc/Hura8/Components/User/AdminController/AUserContactController.php @@ -0,0 +1,27 @@ +objUserContactModel = new UserContactModel(); + parent::__construct($this->objUserContactModel); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // TODO: Implement deleteFileBeforeDeleteItem() method. + return true; + } + +} diff --git a/inc/Hura8/Components/User/AdminController/AUserController.php b/inc/Hura8/Components/User/AdminController/AUserController.php new file mode 100644 index 0000000..bd6eba4 --- /dev/null +++ b/inc/Hura8/Components/User/AdminController/AUserController.php @@ -0,0 +1,18 @@ +objUserReviewModel = new UserReviewModel(); + parent::__construct($this->objUserReviewModel); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // TODO: Implement deleteFileBeforeDeleteItem() method. + return true; + } + +} diff --git a/inc/Hura8/Components/User/Controller/bUserController.php b/inc/Hura8/Components/User/Controller/bUserController.php new file mode 100644 index 0000000..becf701 --- /dev/null +++ b/inc/Hura8/Components/User/Controller/bUserController.php @@ -0,0 +1,35 @@ +web_user_id = self::getWebUserId(); + $this->objUserModel = new UserModel($this->web_user_id ); + parent::__construct($this->objUserModel); + } + + + /** + * @description this value is set by hura.js for anonymous web users + * @return string + */ + public static function getWebUserId() { + return Cookie::get("uID", ''); + } + + + +} diff --git a/inc/Hura8/Components/User/Controller/bUserReviewController.php b/inc/Hura8/Components/User/Controller/bUserReviewController.php new file mode 100644 index 0000000..d08bc58 --- /dev/null +++ b/inc/Hura8/Components/User/Controller/bUserReviewController.php @@ -0,0 +1,27 @@ +web_user_id = UUserController::getWebUserId(); + + $this->objUserReviewModel = new UserReviewModel($item_type, $item_id); + + parent::__construct($this->objUserReviewModel); + } + + + +} diff --git a/inc/Hura8/Components/User/Controller/bUserUploadController.php b/inc/Hura8/Components/User/Controller/bUserUploadController.php new file mode 100644 index 0000000..731c7e3 --- /dev/null +++ b/inc/Hura8/Components/User/Controller/bUserUploadController.php @@ -0,0 +1,41 @@ +web_user_id = AUserController::getWebUserId(); + $this->customer_id = UCustomerLoginController::getLoggedInCustomerId(); + + $this->item_type = $item_type; + $this->item_id = $item_id; + + parent::__construct(new UserUploadModel($this->web_user_id, $this->customer_id, $item_type, $item_id)); + } + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // delete thumb files + $item_info = $this->getInfo($item_id); + + if($item_info['file_path']) { + $file_local_path = PUBLIC_DIR . "/". $item_info['file_path']; + @unlink($file_local_path); + } + + // ok + return true; + } + +} diff --git a/inc/Hura8/Components/User/Model/NewsletterModel.php b/inc/Hura8/Components/User/Model/NewsletterModel.php new file mode 100644 index 0000000..4e25fff --- /dev/null +++ b/inc/Hura8/Components/User/Model/NewsletterModel.php @@ -0,0 +1,73 @@ + "", + "status" => 0, + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + $info = $input_info; + + $info['create_time'] = CURRENT_TIME; + + return new AppResponse('ok', null, $info); + } + + protected function afterCreateItem($new_item_id, $new_item_info) + { + // TODO: Implement afterCreateItem() method. + } + + protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info) : AppResponse + { + return new AppResponse('ok', null, $new_input_info); + } + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) + { + // TODO: Implement afterUpdateItem() method. + } + + protected function beforeDeleteItem($item_id, $item_info) : AppResponse + { + return new AppResponse('ok'); + } + + protected function afterDeleteItem($item_id, $item_info) + { + // TODO: Implement afterDeleteItem() method. + } +} diff --git a/inc/Hura8/Components/User/Model/UserCommentModel.php b/inc/Hura8/Components/User/Model/UserCommentModel.php new file mode 100644 index 0000000..7a18f1c --- /dev/null +++ b/inc/Hura8/Components/User/Model/UserCommentModel.php @@ -0,0 +1,90 @@ + "", + "status" => 0, + 'read' => 1,-1 + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + if(isset($condition["read"]) && $condition["read"]){ + $catCondition[] = " AND `is_read` = ? "; + $bind_types[] = 'd'; + $bind_values[] = ($condition["read"] == 1) ? 1 : 0; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + $info = $input_info; + + $info['create_time'] = CURRENT_TIME; + $info['create_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + protected function afterCreateItem($new_item_id, $new_item_info) + { + + } + + protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info) : AppResponse + { + $info = $new_input_info; + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) + { + + } + + protected function beforeDeleteItem($item_id, $item_info) : AppResponse + { + return new AppResponse('ok'); + } + + protected function afterDeleteItem($item_id, $item_info) + { + + } + +} diff --git a/inc/Hura8/Components/User/Model/UserCommentReplyModel.php b/inc/Hura8/Components/User/Model/UserCommentReplyModel.php new file mode 100644 index 0000000..d21dc50 --- /dev/null +++ b/inc/Hura8/Components/User/Model/UserCommentReplyModel.php @@ -0,0 +1,96 @@ + "", + "status" => 0, + 'read' => 1,-1 + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + if(isset($condition["read"]) && $condition["read"]){ + $catCondition[] = " AND `is_read` = ? "; + $bind_types[] = 'd'; + $bind_values[] = ($condition["read"] == 1) ? 1 : 0; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + protected function _buildQueryOrderBy($sort_by = "new") + { + return parent::_buildQueryOrderBy($sort_by); + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + $info = $input_info; + + $info['create_time'] = CURRENT_TIME; + $info['create_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + protected function afterCreateItem($new_item_id, $new_item_info) + { + + } + + protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info) : AppResponse + { + $info = $new_input_info; + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) + { + + } + + protected function beforeDeleteItem($item_id, $item_info) : AppResponse + { + return new AppResponse('ok'); + } + + protected function afterDeleteItem($item_id, $item_info) + { + + } + +} diff --git a/inc/Hura8/Components/User/Model/UserContactModel.php b/inc/Hura8/Components/User/Model/UserContactModel.php new file mode 100644 index 0000000..bbde4b3 --- /dev/null +++ b/inc/Hura8/Components/User/Model/UserContactModel.php @@ -0,0 +1,54 @@ + "", + "status" => 0, + 'read' => 1,-1 + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + if(isset($condition["read"]) && $condition["read"]){ + $catCondition[] = " AND `is_read` = ? "; + $bind_types[] = 'd'; + $bind_values[] = ($condition["read"] == 1) ? 1 : 0; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + protected function _buildQueryOrderBy($sort_by = "new") + { + return parent::_buildQueryOrderBy($sort_by); + } + +} diff --git a/inc/Hura8/Components/User/Model/UserModel.php b/inc/Hura8/Components/User/Model/UserModel.php new file mode 100644 index 0000000..63881aa --- /dev/null +++ b/inc/Hura8/Components/User/Model/UserModel.php @@ -0,0 +1,58 @@ +web_user_id = $web_user_id; + $this->user_db_id = $user_db_id; + + parent::__construct('web_user_info'); + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + protected function _buildQueryConditionExtend(array $condition) : ?array + { + /*$condition = array( + "q" => "", + "status" => 0, + );*/ + + /*$condition = array( + "letter" => "", + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + if(isset($filter_condition["letter"]) && strlen($filter_condition["letter"]) == 1){ + $catCondition[] = " AND `letter` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["letter"]; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + +} diff --git a/inc/Hura8/Components/User/Model/UserReviewModel.php b/inc/Hura8/Components/User/Model/UserReviewModel.php new file mode 100644 index 0000000..336df9c --- /dev/null +++ b/inc/Hura8/Components/User/Model/UserReviewModel.php @@ -0,0 +1,218 @@ + true, + 'auto-approve-message' => false, + 'detect-spam-before-create' => true, + ]; + + protected $item_type = ''; + protected $item_id = 0; + + /* @var ?iEntityStatistic $iEntityStatisticModel */ + protected $iEntityStatisticModel; + + public function __construct($item_type, $item_id = 0) { + + $this->item_type = $item_type; + $this->item_id = $item_id; + + $this->iEntityStatisticModel = get_statistic_model_instance($this->item_type); + + // todo: overwrite default rules by client + + parent::__construct("user-review"); + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function getSummary(array $conditions = []) + { + $where_conditions = []; + $bind_types = []; + $bind_values = []; + + //item_type + $where_conditions[] = " AND `item_type` = ? "; + $bind_types[] = 's'; + $bind_values[] = $this->item_type; + + + //item_id + $where_conditions[] = " AND `item_id` = ? "; + $bind_types[] = 's'; + $bind_values[] = $this->item_id; + + + //approved + if(isset($conditions["approved"]) ){ + $where_conditions[] = " AND `approved` = ? "; + $bind_types[] = 'd'; + $bind_values[] = ($conditions["approved"] == 1) ? 1 : 0; + } + + $query = $this->db->runQuery( + "SELECT AVG(`rate`) AS avgRate, COUNT(*) AS total FROM `". $this->tb_entity ."` WHERE 1 ".join(" ", $where_conditions), + $bind_types, $bind_values + ); + + if ( $info = $this->db->fetchAssoc($query) ) { + return [ + "avgRate" => $info['avgRate'], + "total" => $info['total'], + ]; + } + + return [ + "avgRate" => 0, + "total" => 0, + ]; + } + + + protected function _buildQueryConditionExtend(array $condition) : ?array + { + /*$condition = array( + "q" => "", + "status" => 0, + 'read' => 1,-1 + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + if(isset($condition["read"]) && $condition["read"]){ + $catCondition[] = " AND `is_read` = ? "; + $bind_types[] = 'd'; + $bind_values[] = ($condition["read"] == 1) ? 1 : 0; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + //todo: check user's authentication and authorization + if($this->rules['require-user-login'] && !UCustomerLoginController::getLoggedInCustomerId()) { + return new AppResponse('error', "Login required"); + } + + $info = $input_info; + + $info['item_type'] = $this->item_type; + $info['item_id'] = $this->item_id; + + $info['item_title'] = DataClean::limitLengthFullWords($input_info['item_title'], 50); + $info['title'] = DataClean::limitLengthFullWords($input_info['title'], 50); + $info['content'] = DataClean::limitLengthFullWords($input_info['content'], 500); + + $info['user_id'] = ''; + $info['user_email'] = DataClean::makeInputSafe($input_info['user_email'], DataType::EMAIL); + $info['user_name'] = DataClean::limitLengthFullWords($input_info['user_name'], 50); + + // todo: check files actually exists and belong to this user + $info['files'] = (is_array($input_info['files'])) ? DataClean::makeListOfInputSafe($input_info['files'], DataType::INTEGER) : null; + + //$info['approved'] = (CONFIG_AUTO_APPROVE_REVIEW) ? 1 : 0; + $info['rate'] = (in_array($info['rate'], [1,2,3,4,5])) ? $info['rate'] : 0; + $info['ip_address'] = USER_IP; + $info['user_agent'] = substr(USER_AGENT, 0, 200); + + /* + "item_type" => $post_info['item_type'], + "item_id" => $post_info['item_id'], + "item_title" => $post_info['item_title'], + "is_user_admin" => $post_info['is_user_admin'], + "user_id" => USER_ID, + "user_email" => $post_info['user_email'], + "user_name" => $post_info['user_name'], + "user_avatar" => $post_info['user_avatar'], + "user_note" => $post_info['user_note'], + "rate" => (int) $post_info['rate'], + "title" => $post_info['title'], + "content" => $post_info['content'], + "files" => $post_info['files'], + "approved" => (CONFIG_AUTO_APPROVE_REVIEW) ? 1 : 0, + "post_time" => CURRENT_TIME, + "ip_address" => USER_IP, + "user_agent" => substr(USER_AGENT, 0, 200),*/ + + $info['create_time'] = CURRENT_TIME; + + return new AppResponse('ok', null, $info); + } + + + protected function afterCreateItem($new_item_id, $new_item_info) + { + // update summary for item + $this->updateItemReviewCount(); + } + + + protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info) : AppResponse + { + //todo: check user's authentication and authorization + + $info = $new_input_info; + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ''; + + return new AppResponse('ok', null, $info); + } + + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) + { + + } + + + protected function beforeDeleteItem($item_id, $item_info) : AppResponse + { + //todo: check user's authentication and authorization + + return new AppResponse('ok'); + } + + + protected function afterDeleteItem($item_id, $item_info) + { + // update summary for item + $this->updateItemReviewCount(); + } + + + protected function updateItemReviewCount() + { + // update summary for item + if($this->iEntityStatisticModel) { + $summary = $this->getSummary([]); + $this->iEntityStatisticModel->updateReviewCount($this->item_id, $summary['total'], $summary['avgRate']); + } + } +} diff --git a/inc/Hura8/Components/User/Model/UserUploadModel.php b/inc/Hura8/Components/User/Model/UserUploadModel.php new file mode 100644 index 0000000..08032c2 --- /dev/null +++ b/inc/Hura8/Components/User/Model/UserUploadModel.php @@ -0,0 +1,63 @@ +web_user_id = $web_user_id; + $this->customer_id = $customer_id; + $this->item_id = $item_id; + $this->item_type = $item_type; + } + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + protected function _buildQueryConditionExtend(array $condition) : ?array + { + /*$condition = array( + "q" => "", + "status" => 0, + 'read' => 1,-1 + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + if(isset($condition["type"]) && $condition["type"]){ + $catCondition[] = " AND `type` = ? "; + $bind_types[] = 's'; + $bind_values[] = $condition["type"]; + } + + if(isset($condition["approved"]) && $condition["approved"]){ + $catCondition[] = " AND `approved` = ? "; + $bind_types[] = 'd'; + $bind_values[] = ($condition["approved"] == 1) ? 1 : 0; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + +} diff --git a/inc/Hura8/Database/ConnectDB.php b/inc/Hura8/Database/ConnectDB.php new file mode 100644 index 0000000..457cbdc --- /dev/null +++ b/inc/Hura8/Database/ConnectDB.php @@ -0,0 +1,503 @@ +debug = $debug; // (defined('ENABLE_DB_DEBUG') && ENABLE_DB_DEBUG); + if($db_id) $this->db_id = $db_id; + + if(!sizeof(self::$cnn_props)) { + self::$cnn_props = self::setConnectionSettings(); + } + } + + private static function setConnectionSettings() { + return static::getCache("getConnectionSettings", function (){ + $db_file = CONFIG_DIR.'/db.php'; + if(file_exists($db_file)) { + return include $db_file; + } + + return []; + }); + } + + /** + * @param string $table + * @return array + */ + public function getTableDefaultItemInfo($table) { + $column_info = $this->getTableInfo($table); + $default_info = []; + foreach ($column_info as $field => $info) { + $default_info[$field] = $info['COLUMN_DEFAULT']; + } + + return $default_info; + } + + + //28-07-2015 get all columns of a table + public function getTableInfo($table) { + + return self::getCache('getTableInfo-'.$table, function () use ($table) { + + $db_props = $this->getConnectionProps(); + if(!$db_props) { + return false; + } + + $database = $db_props['db'] ?? null; + if(!$database) { + return false; + } + + $query = $this->runQuery( + "SELECT + `COLUMN_NAME` , + COLUMN_DEFAULT, + ORDINAL_POSITION, + COLUMN_DEFAULT, + DATA_TYPE, + CHARACTER_MAXIMUM_LENGTH, + COLUMN_TYPE, + COLUMN_KEY, + EXTRA + FROM INFORMATION_SCHEMA.COLUMNS + WHERE `TABLE_SCHEMA` = ? AND`TABLE_NAME` = ? ", + ['s', 's'], [$database, $table] + ); + + $output = []; + foreach( $this->fetchAll($query) as $row){ + $output[$row['COLUMN_NAME']] = $row; + } + + return $output; + }); + } + + + protected function setTrace($query, $start_time, $msg = ''){ + static::$traces[] = [ + "msg" => $msg, + "query" => $query, + "start_time" => $start_time, + "total_time" => round($this->getCurrentTime() - $start_time, 5), + ]; + } + + + public static function getTraces() { + $query_count = sizeof(static::$traces); + $query_time = array_sum(array_map(function ($item){ return $item['total_time'];}, static::$traces)) ; + return [ + 'query_count' => $query_count, + 'query_time' => $query_time, + 'list' => static::$traces, + ]; + } + + + private function __clone(){ + // + } + + + /** + * @param string $db_id + * @param bool $debug + * @return iConnectDB + */ + public static function getInstance(string $db_id = '', bool $debug = false) { + if( ! $db_id ) $db_id = 'main'; + + if(!isset(ConnectDB::$instance[$db_id])) { + ConnectDB::$instance[$db_id] = new self($db_id, $debug); + } + + return ConnectDB::$instance[$db_id]; + } + + + //close all connections + public static function close() { + foreach (ConnectDB::$instance as $db_id => $cnn) { + $cnn->disconnect(); + } + // reset + ConnectDB::$instance = []; + } + + + public function isConnected(): bool { + return ($this->connection instanceof \mysqli); + } + + + private function getConnectionProps() { + return self::$cnn_props[$this->db_id] ?? null; + } + + + private function connect() + { + // already connect + if($this->connection instanceof \mysqli) { + return true; + } + + // connection props not set + $db_props = $this->getConnectionProps(); + if(!$db_props) { + return false; + } + + $host = $db_props['host'] ?? null; + $user = $db_props['user'] ?? null; + $pass = $db_props['pass'] ?? null; + $database = $db_props['db'] ?? null; + $db_charset = $db_props['charset'] ?? 'latin1'; + + if(!$host || !$user || !$pass || !$database) { + return false; + } + + try { + + //create the object + $cnn = \mysqli_init(); + $cnn->options(MYSQLI_OPT_CONNECT_TIMEOUT, 5); + //specify the read timeout + if (!defined('MYSQLI_OPT_READ_TIMEOUT')) { + define ('MYSQLI_OPT_READ_TIMEOUT', 11); + } + $cnn->options(MYSQLI_OPT_READ_TIMEOUT, 10); + + if($cnn->real_connect($host, $user, $pass)){ + $cnn->select_db($database); + $cnn->query("SET NAMES ".$db_charset); // UTF8|latin1 + $cnn->query("SET sql_mode=''"); //for old version to work with mysql 5.7 + //mysqli_select_db($this->connection, $database); + //mysqli_query($this->connection, "SET NAMES UTF8") ;//set utf8 if database using utf-8 encode + //mysqli_query($this->connection, "SET NAMES latin1") ;//set back to latin1 + //mysqli_query($this->connection, "SET sql_mode=''") ;//for old version to work with mysql 5.7 + $this->connection = $cnn; + + return true; + } + + //throw new \Exception('Unable to connect'); + return false; + + }catch (\Exception $e) { + //die($e->getMessage()); + echo $e->getMessage(); + + return false; + } + } + + + public function disconnect() + { + if($this->connection instanceof \mysqli) { + $this->connection->close(); + $this->connection = null; + } + } + + + public function ping() { + if($this->connection) { + $this->connection->ping(); + } + } + + + /** + * @param string $query + * @param array $bind_types + * @param array $bind_values + * @param bool $get_affected_row_or_id get the affected row or newly insert-id from the query, default return the mysqli_result + * @return \mysqli_result | false | int + */ + public function runQuery($query, array $bind_types=[], array $bind_values=[], $get_affected_row_or_id = false) + { + $start_time = $this->getCurrentTime(); + + // connect on demand + if(!$this->connect()) { + if($this->debug) $this->setTrace($query, $start_time, 'runQuery: Connection fails'); + return false; + } + + $stmt = $this->connection->prepare( $query ); + if(!$stmt) { + //throw new \Exception($stmt->error); + //throw new \Exception($this->connection->error); + if($this->debug) { + $this->setTrace($query, $start_time, 'runQuery: '.$this->connection->error); + die("runQuery error: ".$this->connection->error.". Query: ".$query); + } + return false; + } + + if(sizeof($bind_types) && sizeof($bind_types) == sizeof($bind_values)) { + if(!$stmt->bind_param(join('', $bind_types), ...$bind_values)) { + if($this->debug) { + $this->setTrace($query, $start_time, 'runQuery: bind_param '.$this->connection->error); + die('runQuery: bind_param '.$this->connection->error); + } + return false; + } + } + + + if(!$stmt->execute()) { + if($this->debug) { + $this->setTrace($query, $start_time, 'runQuery: execute '.$this->connection->error); + die("runQuery error: ".$this->connection->error.". Query: ".$query); + } + return false; + } + + if($this->debug) $this->setTrace($query, $start_time, ''); + + if($get_affected_row_or_id) { + return $stmt->insert_id ?: $stmt->affected_rows; + } + + // default + return $stmt->get_result(); + } + + + private function getCurrentTime(){ + return microtime(true); + } + + + /** + * @deprecated Unsafe, make sure all variables are properly escaped + */ + public function multi_query(array $array_query){ + + if(!sizeof($array_query)) return false; + + $start_time = $this->getCurrentTime(); + + // late connect + if(!$this->connect()) { + if($this->debug) $this->setTrace(join("; ", $array_query), $start_time, 'multi_query: Connection fails'); + return false; + } + + $multi_query = join(";", $array_query); + $multi_query = str_replace("\n", " ", $multi_query); + //remove double ; if exist + $multi_query = preg_replace("/;(\s+)?;/i", ";", $multi_query); + + $set = 0; + $list = []; + if(mysqli_multi_query($this->connection, $multi_query)){ + // flush multi_queries, so any query after can run + do { + $set ++; + /* store first result set */ + if ($result = mysqli_store_result($this->connection)) { + /*while ($row = mysqli_fetch_assoc($result)) { + $list[$set][] = $row; + }*/ + $list[$set] = $this->fetchAll($result); + mysqli_free_result($result); + } + } while (mysqli_more_results($this->connection) && mysqli_next_result($this->connection)); + } + + if($this->debug) $this->setTrace(join(";", $array_query), $start_time); + + return $list; + } + + + // a simple utitily method which we use very frequently + public function getItemInfo($table_name, $id_value, $id_field = 'id'){ + return $this->select( + $table_name, + [], + [ + $id_field => ["=", $id_value], + ], + '', + 1 + ); + } + + + //$table_name = 'table_abc' + /*$fields = array( + "field_name_1", + );*/ + /*$where_condition = array( + "field_name_1" => ["=", "value_1"], + "field_name_2" => ["LIKE", "value_1"], + );*/ + public function select($table_name, $fields =[], $where_condition=[], $order_by = '', $limit='1'){ + + // connect on demand + if(!$this->connect()) { + if($this->debug) $this->setTrace($table_name, 0, 'select: Connection fails'); + return false; + } + + $start_time = $this->getCurrentTime(); + + $check_fields = []; + if(sizeof($fields) && $fields[0] != '*') $check_fields = $fields; + $check_fields = array_merge($check_fields, array_keys($where_condition)); + + $cols_in_tables = $this->filterColumnInTable($table_name, $check_fields); + + $bind_types = []; + $bind_values = []; + $condition_list = []; + $permitted_operator = [">", ">=", "<", "<=", "!=", "=", "BETWEEN", "LIKE", "IS"]; // https://dev.mysql.com/doc/refman/8.0/en/non-typed-operators.html + foreach ($where_condition as $field_name => $operator_value) { + + // if any field invalid, stop the query + if(!in_array($field_name, $cols_in_tables)) { + echo "Invalid field_name ".$field_name; + return false; + }; + + // invalidate + if(!is_array($operator_value) || sizeof($operator_value) != 2) continue; + + $operator = $operator_value[0]; + if(!in_array($operator, $permitted_operator)) continue; + + $value = $operator_value[1]; + + $condition_list[] = " `".$field_name."` ".$operator." ? "; + $bind_types[] = (is_int($value)) ? 'd' : 's'; + $bind_values[] = $value; + } + + $select_fields = (sizeof($fields) > 0) ? array_map(function ($field){ return "`".$field."`";}, $fields) : ["*"]; + $order_by_query = ($order_by) ? "ORDER BY ".preg_replace("/[^a-z0-9\s_]/i", "", $order_by) : ""; + $safe_limit = ($limit) ? preg_replace("/[^0-9\,\s]/i", "", $limit) : "1"; + $where_condition = (sizeof($condition_list)) ? " AND " . join(" AND ", $condition_list) : ""; + + $query = "SELECT ".join(',', $select_fields)." + FROM `".$table_name."` WHERE 1 ".$where_condition." + ".$order_by_query." + LIMIT ".$safe_limit; + + $stmt = $this->connection->prepare( $query ); + if(!$stmt) return false; + + if(sizeof($bind_types)) { + $stmt->bind_param(join('', $bind_types), ...$bind_values); + } + $stmt->execute(); + $result = $stmt->get_result(); + + if($this->debug) $this->setTrace($query, $start_time); + + return ($safe_limit == '1') ? $result->fetch_assoc() : $result->fetch_all(); + } + + + protected function filterColumnInTable($table_name, array $list_check_fields) { + $table_columns = $this->getTableInfo($table_name); + + $safe_fields = []; + foreach ($list_check_fields as $field_name) { + // make sure the field exists + if(!array_key_exists($field_name, $table_columns)) continue; + + $safe_fields[] = $field_name; + } + + return $safe_fields; + } + + + //fetch array + /** + * @deprecated + */ + public function fetchArray(\mysqli_result $resource){ + return $resource->fetch_array(); + } + + /** + * @param \mysqli_result | false $resource + * @return array|null + */ + public function fetchAssoc( $resource){ + if(!$resource) { + //die("Resource failed: code fetchAssoc 122"); + return null; + } + + return $resource->fetch_assoc(); + } + + /** + * @param \mysqli_result | false $resource + * @return array|mixed + */ + public function fetchAll($resource){ + + if(!$resource) return []; + + //some hosting does not enable this function, so this is the work-around + if( ! function_exists("mysqli_fetch_all")) { + $item_list = array(); + while ($item = $this->fetchAssoc($resource)) { + $item_list[] = $item; + } + + return $item_list; + } + + return ($resource) ? $resource->fetch_all(MYSQLI_ASSOC) : []; + } + + //affected rows from last query + public function get_affected_rows(){ + return $this->connection->affected_rows; + } + + + public function getErrorNo(){ + return mysqli_errno($this->connection); + } + + + public function __destruct() { + $this->disconnect(); + } +} diff --git a/inc/Hura8/Database/MysqlValue.php b/inc/Hura8/Database/MysqlValue.php new file mode 100644 index 0000000..58a5762 --- /dev/null +++ b/inc/Hura8/Database/MysqlValue.php @@ -0,0 +1,16 @@ +value = $value; + } + + public function getValue() { + return $this->value; + } +} diff --git a/inc/Hura8/Database/iConnectDB.php b/inc/Hura8/Database/iConnectDB.php new file mode 100644 index 0000000..ee3151e --- /dev/null +++ b/inc/Hura8/Database/iConnectDB.php @@ -0,0 +1,55 @@ + ["=", "value_1"], + "field_name_2" => ["LIKE", "value_1"], + );*/ + public function select($table_name, $fields =[], $where_condition=[], $order_by = '', $limit='1'); + + + /** + * @param \mysqli_result | false $resource + * @return array|null + */ + public function fetchAssoc( $resource); + + /** + * @param \mysqli_result | false $resource + * @return array|mixed + */ + public function fetchAll($resource); + + public function getErrorNo(); + +} diff --git a/inc/Hura8/Interfaces/APIResponse.php b/inc/Hura8/Interfaces/APIResponse.php new file mode 100644 index 0000000..a5a2f21 --- /dev/null +++ b/inc/Hura8/Interfaces/APIResponse.php @@ -0,0 +1,42 @@ +errCode = $errCode; + $this->msg = $msg; + $this->data = $data; + } + + /** + * @return int + */ + public function getCode() { + return $this->errCode; + } + + /** + * @return mixed if status = 'error' + */ + public function getMsg() { + return $this->msg; + } + + /** + * @return mixed if status = 'ok' + */ + public function getData() { + return $this->data; + } + +} diff --git a/inc/Hura8/Interfaces/AppResponse.php b/inc/Hura8/Interfaces/AppResponse.php new file mode 100644 index 0000000..e48eac4 --- /dev/null +++ b/inc/Hura8/Interfaces/AppResponse.php @@ -0,0 +1,41 @@ +status = $status; + $this->msg = $msg; + $this->data = $data; + } + + /** + * @return string which is 'ok' or 'error' + */ + public function getStatus() { + return $this->status; + } + + /** + * @return mixed if status = 'error' + */ + public function getMsg() { + return $this->msg; + } + + /** + * @return mixed if status = 'ok' + */ + public function getData() { + return $this->data; + } + +} diff --git a/inc/Hura8/Interfaces/EntityType.php b/inc/Hura8/Interfaces/EntityType.php new file mode 100644 index 0000000..237f93b --- /dev/null +++ b/inc/Hura8/Interfaces/EntityType.php @@ -0,0 +1,151 @@ + $clean_file_name, + "public_path" => $this->public_dir . "/".$clean_file_name, + "local_path" => $this->target_dir . "/" . $clean_file_name, + "mime_type" => $mimeType, + "file_size" => $file_size, + "file_ext" => $file_ext, + "width" => 0, + "height" => 0, + ];*/ + + $this->file_name = $file_info['file_name'] ?? '' ; + $this->public_path = $file_info['public_path'] ?? '' ; + $this->local_path = $file_info['local_path'] ?? '' ; + $this->mime_type = $file_info['mime_type'] ?? '' ; + $this->file_size = $file_info['file_size'] ?? 0 ; + $this->file_ext = $file_info['file_ext'] ?? '' ; + $this->width = $file_info['width'] ?? 0 ; + $this->height = $file_info['height'] ?? 0 ; + + } + + +} diff --git a/inc/Hura8/Interfaces/FileHandleResponse.php b/inc/Hura8/Interfaces/FileHandleResponse.php new file mode 100644 index 0000000..360cf06 --- /dev/null +++ b/inc/Hura8/Interfaces/FileHandleResponse.php @@ -0,0 +1,19 @@ + 2000000, + "tb_product.title" => "Máy tính ABC", + "tb_category.price" => "Máy tính", + ]; + * @return bool + */ + public function updateItem($item_id, array $table_field_values = []): AppResponse; + + /** + * @description delete item from index + * @param $item_id + */ + public function deleteItem($item_id): AppResponse; + + /** + * @description delete items from index + * @param $item_list_ids + */ + public function deleteItems(array $item_list_ids): AppResponse; + + /** + * @param string $keyword + * @param array $field_filters + * @param array $fulltext_fields + * @param int $limit_result + * @return array[int] + */ + public function find(string $keyword, array $field_filters = [], array $fulltext_fields = [], int $limit_result = 2000) : array; + +} diff --git a/inc/Hura8/Interfaces/iShippingCost.php b/inc/Hura8/Interfaces/iShippingCost.php new file mode 100644 index 0000000..87d7c76 --- /dev/null +++ b/inc/Hura8/Interfaces/iShippingCost.php @@ -0,0 +1,15 @@ +path_config) { + //$path_config_file = APP_DIR . '/config/routing.php'; + //$this->path_config = require $path_config_file; + //} + } + + // url: admin/abc/product.php?para1=value1 + public function getRouting() { + $parsed = Url::parse($_SERVER['REQUEST_URI']); //abc/product?param1=12¶m2=value2 + // print_r($parsed); + + // home + if($parsed['path'] == '/') { + return [ + 'module' => preg_replace("/[^a-z0-9_\-]/i","", getRequest('module', 'home')), + 'view' => preg_replace("/[^a-z0-9_\-]/i","", getRequest('view', 'home')), + 'view_id'=> 0, + 'query' => $parsed['query'], + 'url' => $_SERVER['REQUEST_URI'], + ]; + } + + // check match pattern in $this->path_config + /*foreach ($this->path_config as $_config => $_route ) { + if(preg_match("{^".$_config."$}", $parsed['path'], $match )) { + + if(isset($_route['query']) && is_array($_route['query'])) { + $_route['query'] = array_merge($_route['query'], $parsed['query']); + }else{ + $_route['query'] = $parsed['query']; + } + + return array_merge([ + 'path' => $parsed['path'], + 'match' => $match, + ], $_route); + } + }*/ + + // auto parse path base on convention: module/view/view_id + $ele = explode("/", $parsed['path']); + + $module = $ele[1] ?? 'home'; + $view = isset($ele[2]) ? $ele[2] : getRequest('view', 'home'); + $view_id = isset($ele[3]) ? $ele[3] : getRequest('id', 'view_id'); + + // else error + return [ + 'module' => preg_replace("/[^a-z0-9_\-]/i","", $module ) , + 'view' => preg_replace("/[^a-z0-9_\-]/i","", $view ) , + 'view_id' => preg_replace("/[^a-z0-9_]/i","", $view_id ), + 'query' => $parsed['query'], + 'url' => $_SERVER['REQUEST_URI'], + ]; + } + +} diff --git a/inc/Hura8/System/APIClient.php b/inc/Hura8/System/APIClient.php new file mode 100644 index 0000000..805fc29 --- /dev/null +++ b/inc/Hura8/System/APIClient.php @@ -0,0 +1,152 @@ +client = new Client([ + // Base URI is used with relative requests + 'base_uri' => $endpoint, + // You can set any number of default request options. + 'timeout' => $timeout, + ]); + } + + + public function post($path, array $parameters) { + return $this->call_service("post", $path, $parameters); + } + + + public function get($path, array $parameters) { + return $this->call_service("get", $path, $parameters); + } + + + public function setHeaders(array $headers) { + // Authorization: + /*$headers = [ + 'Authorization' => "Basic ". base64_encode($this->api_key), + ];*/ + $this->headers = $headers; + } + + //ref: http://docs.guzzlephp.org/en/stable/quickstart.html#query-string-parameters + // make a consistent call with the same $payload format: ["key" => "value", ...] + /* + return json { + "errCode" => 1|0, (1=error, 0=success) + "msg" => "error_message" | "", + }*/ + protected function call_service($http_method, $path, array $payload) { + + try { + + // get + if( $http_method == 'get' ) { + + $response = $this->client->request('GET', $path, [ + 'headers' => $this->headers, + 'query' => $payload + ]); + } + + // post + else { + + $request_options = $this->buildPostRequestOptions($this->headers, $payload); + $response = $this->client->request('POST', $path, $request_options); + } + + return [ + "errCode" => "0", + "msg" => $response->getBody()->getContents(), + ]; + + } + catch (ServerException $e) { + $response = $e->getResponse(); + //$errors = \json_decode($response->getBody()->getContents(), true); + return [ + "errCode" => 1, + "msg" => $response->getBody()->getContents(), + ]; + } + catch (ClientException $e) { + $response = $e->getResponse(); + //$errors = \json_decode($response->getBody()->getContents(), true); + return [ + "errCode" => 2, + "msg" => $response->getBody()->getContents(), + ]; + } + catch (\Exception $e) { + return [ + "errCode" => 3, + "msg" => "Exception: ".$e->getMessage(), + ]; + } + } + + + // ref: http://docs.guzzlephp.org/en/stable/request-options.html + // form_params cannot be used with the multipart option. You will need to use one or the other. + // Use form_params for application/x-www-form-urlencoded requests, and multipart for multipart/form-data requests. + protected function buildPostRequestOptions(array $headers, array $payload) { + $content_type = isset($headers["Content-Type"]) ? $headers["Content-Type"] : ""; + + if($content_type == "application/x-www-form-urlencoded") { + return [ + 'headers' => $headers, + 'form_params' => $payload, + //'debug' => true + ]; + } + + if($content_type == "application/json") { + return [ + 'headers' => $headers, + 'json' => $payload, + //'body' => json_encode($payload), + ]; + } + + + // reformat the payload for multipart + $multipart_request = []; + foreach ($payload as $key => $value) { + if( ! $key) continue; + + $multipart_request[] = [ + 'name' => $key, + 'contents' => (is_array($value)) ? json_encode($value) : $value, + ]; + } + + // multipart/form-data + return [ + 'headers' => $headers, + 'multipart' => $multipart_request, + ]; + + } +} diff --git a/inc/Hura8/System/CDNFileUpload.php b/inc/Hura8/System/CDNFileUpload.php new file mode 100644 index 0000000..421c2eb --- /dev/null +++ b/inc/Hura8/System/CDNFileUpload.php @@ -0,0 +1,152 @@ + 'media/product', // media/product + 'article' => 'media/news', // media/news + 'brand' => 'media/brand', + 'category' => 'media/category', + 'banner' => 'media/banner', + 'lib' => 'media/lib/', + 'user_upload' => 'media/user_upload', + ]; + + public function __construct($user_id) { + $this->user_id = $user_id; + } + + /** + * @description: an implementation of start method to upload an item's image + * @param $item_type string + * @param $content string + * @param $file_name string + * @return array | boolean + */ + public function uploadFile($item_type, $file_name, $content, $set_target_path = '') + { + //$file_name = substr(strrchr($img_url, "/"), 1); + //$content = get_url_content($img_url); + + $header_setting = [ + // 'Authorization' => "Basic ". base64_encode(API_KEY), + ]; + + if($set_target_path) { + $target_path = $set_target_path; + }else{ + $target_path = (isset($this->target_path_item_type_mapping[$item_type])) ? $this->target_path_item_type_mapping[$item_type] : ''; + } + + $multipart_settings = [ + [ + 'name' => 'upload_method', + 'contents' => 'content', + ], + [ + 'name' => 'file_name', + 'contents' => $file_name, + ], + [ + 'name' => 'target_path', + 'contents' => $target_path, + ], + [ + 'name' => 'file_content', + 'contents' => $content, + ], + + //for upload server + [ + 'name' => 'uid', + 'contents' => $this->user_id, + ], + [ + 'name' => 'time', + 'contents' => CURRENT_TIME, + ], + [ + 'name' => 'token', + 'contents' => $this->createToken($this->user_id, CURRENT_TIME), + ], + [ + 'name' => 'item_type', + 'contents' => $item_type , + ], + ]; + + return $this->start($header_setting, $multipart_settings); + } + + /* + let settings = { + element : '',//element id to click on + holder : '', //id file holder + form_name : '', + item_type : '', + item_id : '', + file_type : '', //what is the file type: doc, photo, video ? + file_field : '', //what field in the item table this file is used for ? + resize : '200,400,600' //sizes to resize + square: '200,400', //sizes to be square + keep_width : '400', //sizes to keep the width + max_file: 1 + } + */ + + /** + * @description: a general method for upload + * @param array $header_setting + * @param array $multipart_settings + * @return array | boolean + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function start(array $header_setting = [], array $multipart_settings = []) { + $client = new \GuzzleHttp\Client(); + + $request = $client->request('POST', $this->upload_handle, [ + 'headers' => $header_setting, + 'multipart' => $multipart_settings + ]); + + return $request->getBody()->getContents(); + } + + + // return string | boolean + protected function getAssetType($file_name) { + $ext = strtolower(strrchr($file_name, ".")); + + if( $ext == '.css') return 'css'; + + if( $ext == '.js') return 'js'; + + if( \in_array($ext, ['.jpg', '.jpeg', '.gif', '.png', '.ico'])) return 'image'; + + return false; + } + + + // this salt and createToken must be the same ones as in Hura8/Base/CDNFileUploadHandle + // and different per project (or else others will use it to upload forbidden files on our clients' hosting) + const SECURITY_SALT = 'ahss@3asdaaSDFSD'; + + protected function createToken($id, $time) { + return sha1(join("-", [$id , $time , static::SECURITY_SALT])); + } + +} + diff --git a/inc/Hura8/System/CDNFileUploadHandle.php b/inc/Hura8/System/CDNFileUploadHandle.php new file mode 100644 index 0000000..c4844e6 --- /dev/null +++ b/inc/Hura8/System/CDNFileUploadHandle.php @@ -0,0 +1,275 @@ + 2000000,//bytes ~ 1MB + 'allowed_file_types' => [ + 'image' => ['.jpeg', '.jpg', '.gif', '.png'], + 'script' => ['.css', '.js'], + ], + + //2. uploaded by client + 'uid' => '', + 'token' => '', + 'time' => '', + 'item_type' => 'product', // product|article|media + 'target_path' => '', + 'upload_method' => 'content', //file || content + "file_content" => "", //only needed if upload_method = content + "file_name" => "",//only needed if upload_method = content + ]; + private $tmp_file_prop = null; //array, temporary file's props in tmp folder + private $accepted_file_input_names = [ + "file", // + //"qqfile", //name use for files uploaded through FineUploader library + ]; + private $user_tmp_dir = ''; + + public function __construct(){ + + } + + protected function createToken($id, $time) { + return sha1(join("-", [$id , $time , static::SECURITY_SALT])); + } + + public function start() { + + $this->_get_post_info(); + + if(!$this->validateUser()) { + return $this->set_return_result('error', 'User failed to verify', []); + } + + //receive files + $file = $this->receive_file(); + + //return file-location to upload API to return to client application + return $this->set_return_result('success', 'Upload succeeded', $file ); + } + + protected function validateUser() { + return ($this->config['token'] == $this->createToken($this->config['uid'], $this->config['time'])); + } + + protected function _get_post_info() { + $expected_keys = [ + 'upload_method', + 'file_name', + 'target_path', + 'file_content', + 'uid', + 'time', + 'token', + 'item_type', + ]; + + foreach ($expected_keys as $key) { + $this->config[$key] = isset($_POST[$key]) ? $_POST[$key] : null; + } + } + + + //return boolean + protected function remove_tmp_file() { + $this->deleteDirectory($this->user_tmp_dir); + + return true; + } + + //return boolean + protected function validate_file() { + //$this->tmp_file_prop + //validate file size + if($this->config['max_file_size'] && $this->config['max_file_size'] < $this->tmp_file_prop['size']) { + return 'Size too large'; + } + + //validate allowed extension: allow image but upload .docx files + $has_extension = false; + $file_type = ''; + foreach ( $this->config['allowed_file_types'] as $group => $group_ext ) { + if(in_array( $this->tmp_file_prop['ext'], $group_ext )) { + $has_extension = true; + $file_type = $group; + break ; + } + } + if( ! $has_extension ) return "File type not allowed"; + + //validate claimed extension: claim image but not actual image + $full_file_path = $this->tmp_file_prop['tmp_location'] . DIRECTORY_SEPARATOR . $this->tmp_file_prop['name']; + if( $file_type == 'image' && ! v::image()->validate( $full_file_path )) { + return "Not actual image"; + } + + return ''; + } + + + //return mixed : array $tmp_file_prop or false + protected function receive_file() { + + //check upload method + if( $this->config['upload_method'] == 'content' ) { + //upload by content - file created on server + $file_ext = $this->get_ext($this->config['file_name']); + $upload_folder = $this->create_upload_folder() ; + + if( ! $this->move_file($upload_folder)) { + return false; + } + + return [ + "ext" => $file_ext, + "name" => $this->config['file_name'], + "folder" => $upload_folder, + "size" => strlen($this->config['file_content']), + ]; + + } else { + + //upload by file + //get list of files to be uploaded + $file_uploaded = null; + foreach ($this->accepted_file_input_names as $_name) { + if(isset($_FILES[$_name])) { + $file_uploaded = $_FILES[$_name]; + break; + } + } + + if ( ! $file_uploaded ) return false; + + //if file name too long, then error + if ( strlen($file_uploaded['name']) > 225 ) return false; + + //move file to tmp folder + $file_ext = $this->get_ext($file_uploaded['name']); + $upload_folder = $this->create_upload_folder() ; + + if( ! $this->move_file($upload_folder) ){ + return false; + } + + return [ + "ext" => $file_ext, + "name" => $this->config['file_name'], + "folder" => $upload_folder, + "size" => $file_uploaded['size'], + ]; + } + } + + + protected function set_return_result($status = 'success', $message = 'Upload success', $content = []) { + return [ + "status" => $status, + "message" => $message, + "files" => $content, + ]; + } + + //create upload folder for user: use current date, user id or app id based on $this->config + private function create_upload_folder() { + //$build_folder = [$this->tmp_dir]; + /*$user_id = (isset($this->config['user_id'])) ? intval($this->config['user_id']) : null; + if($user_id) { + $build_folder[] = $user_id; + } + $build_folder[] = date("Ymd");*/ + + //rebuild upload folder + $build_folder = array_filter(explode("/", $this->config['target_path'])); + $folder = join("/", $build_folder); + $this->createDir($folder); + + return $folder; + } + + //get a file's extension + private function get_ext($fileName){ + return strtolower(strrchr($fileName, '.')); + } + + // check if find is image + private function isImage($fileName) { + return (in_array( $this->get_ext($fileName), ['.jpeg', '.jpg', '.gif', '.png'] )); + } + + + //for security reason: only allow a-z0-9_- in file name (no other dot . except for the extension) + //example: bad-file.php.js -> bad-filephp.js + private function rename_file($uploaded_file_name, $ext) { + $new_name = preg_replace("/[^a-z0-9_\-]/i", "", str_replace( strrchr($uploaded_file_name, '.'), "", $uploaded_file_name ) ) . $ext; + + return strtolower($new_name); + } + + + private function move_file($folder) { + $new_file = $folder . '/' . $this->config['file_name']; + + if( $this->config['upload_method'] == 'content' ) { + // image upload by content + if( $this->isImage($new_file) ) { + //Store in the filesystem. + $fp = fopen($new_file, "w+"); + $status = fwrite($fp, $this->config['file_content']); + fclose($fp); + + return ($status !== false); + } + // file upload + else if( file_put_contents($new_file, $this->config['file_content'])){ + return true; + } + } + + return false; + } + + + protected function createDir($path, $folder_permission = 0750){ + if(file_exists($path)) { + return true; + } + + return mkdir($path, $folder_permission, true); + } + + protected function deleteDirectory($dirPath) { + if (!is_dir($dirPath)) { + return false; + } + + $objects = scandir($dirPath); + foreach ($objects as $object) { + if ($object != "." && $object !="..") { + if (filetype($dirPath . DIRECTORY_SEPARATOR . $object) == "dir") { + $this->deleteDirectory($dirPath . DIRECTORY_SEPARATOR . $object); + } else { + unlink($dirPath . DIRECTORY_SEPARATOR . $object); + } + } + } + + return rmdir($dirPath); + } + +} diff --git a/inc/Hura8/System/Cache.php b/inc/Hura8/System/Cache.php new file mode 100644 index 0000000..a9b3b89 --- /dev/null +++ b/inc/Hura8/System/Cache.php @@ -0,0 +1,100 @@ + 10, + //'retry_interval' => 2, + /* + 'persistent' => 1, + 'persistent_id' => null, + 'timeout' => 10, + + 'tcp_keepalive' => 0, + 'lazy' => null, + 'redis_cluster' => false, + 'redis_sentinel' => null,*/ + ] ); + + //var_dump($redisConnection->isConnected()); + + }catch (\Exception $exception) { + //echo $exception->getMessage(); + $redisConnection = false; + } + + if(!$redisConnection) { + static::$redisCache = false; + return false; + } + + static::$redisCache = new RedisAdapter( + // the object that stores a valid connection to your Redis system + $redisConnection, + + // the string prefixed to the keys of the items stored in this cache + $namespace = '', + + // the default lifetime (in seconds) for cache items that do not define their + // own lifetime, with a value 0 causing items to be stored indefinitely (i.e. + // until RedisAdapter::clear() is invoked or the server(s) are purged) + $defaultLifetime = 10 + ); + + return static::$redisCache; + + } + + +} diff --git a/inc/Hura8/System/Config.php b/inc/Hura8/System/Config.php new file mode 100644 index 0000000..a9e8a79 --- /dev/null +++ b/inc/Hura8/System/Config.php @@ -0,0 +1,208 @@ + 'Hình sản phẩm', + ]; + } + + return include $config_image_type_file; + } + + + public static function getProductUnitList() { + return static::getCache("getProductUnitList", function (){ + $config_file = CONFIG_DIR . "/client/product.unit.php"; + if(file_exists($config_file)) { + return include $config_file; + } + + return []; + }); + } + + + public static function getLanguageCount(){ + $language_list = static::getLanguageConfig(); + $count = sizeof($language_list); + return ($count > 1) ? $count : 1; //always have at least 1 language + } + + + public static function getLanguageConfig(){ + return static::getCache('getLanguageConfig', function (){ + $config_file = CONFIG_DIR . "/client/language_enable.php"; + if(!file_exists($config_file)) { + return []; + } + + return include $config_file; + }); + } + + + public static function getClientBuildConfig() { + return static::getCache("getClientBuildConfig", function (){ + $config_file = CONFIG_DIR . '/build/store.config.php'; + if(file_exists($config_file)) { + return include $config_file; + } + + return []; + }); + } + + +} diff --git a/inc/Hura8/System/Constant.php b/inc/Hura8/System/Constant.php new file mode 100644 index 0000000..67d8d2d --- /dev/null +++ b/inc/Hura8/System/Constant.php @@ -0,0 +1,106 @@ + $title) { + $extended_types[] = preg_replace("/[^a-z0-9_\-]/i", "", $key); + } + } + + $list = []; + try { + $reflectionClass = new \ReflectionClass(new \Hura8\Interfaces\EntityType()); + foreach ($reflectionClass->getConstants() as $handle => $value) { + $list[] = $value; + } + + // add extend + foreach ($extended_types as $type) { + if(!in_array($type, $list)) { + $list[] = $type; + } + } + + } catch (\ReflectionException $e) { + // + } + + return $list; + }); + } + + + // list of languages can be enabled + public static function languagePermitList() { + return [ + "vi" => "Tiếng Việt", // default + "en" => "English", + ]; + } + + + public static function mobileProviderPrefixList() { + return static::getCache('mobileProviderPrefixList', function () { + return include static::$constant_dir. '/mobile_provider.php'; + }); + } + + + public static function customerStageList() { + return static::getCache('customerStageList', function () { + return include static::$constant_dir. '/customer_stage_list.php'; + }); + } + + + public static function saluteList() { + return static::getCache('genderList', function () { + return include static::$constant_dir. '/salute_list.php'; + }); + } + + + public static function genderList() { + return static::getCache('genderList', function () { + return include static::$constant_dir. '/gender_list.php'; + }); + } + + + public static function industryList() { + return static::getCache('industryList', function () { + return include static::$constant_dir. '/industry_list.php'; + }); + } + + + public static function maritalStatusList() { + return static::getCache('maritalStatusList', function () { + return include static::$constant_dir. '/marital_status_list.php'; + }); + } + +} diff --git a/inc/Hura8/System/Controller/DomainController.php b/inc/Hura8/System/Controller/DomainController.php new file mode 100644 index 0000000..b239898 --- /dev/null +++ b/inc/Hura8/System/Controller/DomainController.php @@ -0,0 +1,124 @@ +objDomainModel = new DomainModel(); + } + + protected $layout_options = [ + "pc" => "Chỉ cho PC", + "mobile" => "Chỉ cho Mobile", + //"amp" => "Chỉ cho AMP", + "all" => "Cả PC & Mobile", + ]; + + + public function buildDomainConfig() { + $domain_per_languages = $this->getList(''); + + $config_domain_list = []; //all domains and attributes, so we can know the info of currently visited domain + $config_domain_languages = []; //domains per language, so we can redirect to main domain of a specific language + + foreach ($domain_per_languages as $lang => $list_domains) { + foreach ($list_domains as $_item) { + $config_domain_languages[$lang][] = [ + "domain" => $_item['domain'], + "is_main" => $_item['isMain'], + "layout" => ($_item['layout']) ? $_item['layout'] : 'pc', + ]; + + $config_domain_list[$_item['domain']] = [ + "lang" => $_item['lang'], + "is_main" => $_item['isMain'], + "layout" => ($_item['layout']) ? $_item['layout'] : 'pc', + ]; + } + } + + return [ + "language" => $config_domain_languages, + "list" => $config_domain_list, + ]; + } + + + public function getLayoutOption(){ + return $this->layout_options; + } + + + public function getList($language = '') { + + $item_list = $this->objDomainModel->getList([ + "language" => $language, + "numPerPage" => 100, + ]); + + $result = array(); + foreach ( $item_list as $rs ) { + if(!$rs['lang']) $rs['lang'] = DEFAULT_LANGUAGE; + $result[$rs['lang']][] = $rs; + } + + return $result; + } + + + public function addNewDomain($domain, $language = DEFAULT_LANGUAGE){ + $this->objDomainModel->addNewDomain($this->cleanDomain($domain), $language); + + $this->rebuildConfigFile(); + } + + + public function deleteDomain($domain){ + $this->objDomainModel->deleteDomain($this->cleanDomain($domain)); + + $this->rebuildConfigFile(); + } + + + public function setDomainMain($domain, $language){ + $this->objDomainModel->setDomainMain($this->cleanDomain($domain), $language); + + $this->rebuildConfigFile(); + } + + + public function setDomainLayout($domain, $layout = 'pc'){ + $layout_option = $this->getLayoutOption(); + if(!isset($layout_option[$layout])) { + return false; + } + + $this->objDomainModel->setDomainLayout($this->cleanDomain($domain), $layout); + $this->rebuildConfigFile(); + + return true; + } + + + protected function cleanDomain($domain) { + $domain_element = parse_url($domain); + + $scheme = isset($domain_element['scheme']) ? $domain_element['scheme'] . '://' : ''; + $host = $domain_element['host'] ?? ''; + $port = isset($domain_element['port']) ? ':' . $domain_element['port'] : ''; + + return strtolower(trim($scheme . $host . $port)); + } + + + protected function rebuildConfigFile() { + $objSettingController = new SettingController(); + $objSettingController->create_config_file_n_upload(); + } +} diff --git a/inc/Hura8/System/Controller/EntityPermissionController.php b/inc/Hura8/System/Controller/EntityPermissionController.php new file mode 100644 index 0000000..1dd7721 --- /dev/null +++ b/inc/Hura8/System/Controller/EntityPermissionController.php @@ -0,0 +1,43 @@ +objRelationModel = new RelationModel($main_item_type, $main_item_id); + $this->main_item_type = $main_item_type; + $this->main_item_id = $main_item_id; + } + + + public function updateOrdering($related_item_id, $new_order) { + return $this->objRelationModel->updateOrdering($related_item_id, $new_order); + } + + + //@warn: this does not check if records exist. + public function create(array $related_items, $both_way_relation = true) { + return $this->objRelationModel->create($related_items, $both_way_relation); + } + + + public function checkExist($main_item_type, $main_item_id, $related_item_type, $related_item_id){ + return $this->objRelationModel->checkExist($main_item_type, $main_item_id, $related_item_type, $related_item_id); + } + + //remove a related-item + public function remove($related_item_type, $related_item_id, $remove_both_way = true) { + return $this->objRelationModel->remove($related_item_type, $related_item_id, $remove_both_way); + } + + + //remove all relate items + public function truncate() { + $this->objRelationModel->truncate(); + } + + + public function getRelatedItems(array $related_item_types = []) { + return $this->objRelationModel->getRelatedItems($related_item_types); + } + + public function getRelatedItemsForList(array $main_item_list_ids, array $related_item_types = []) { + return $this->objRelationModel->getRelatedItemsForList($main_item_list_ids, $related_item_types); + } + + //count related items + public function getRelatedItemCount() { + return $this->objRelationModel->getRelatedItemCount(); + } + + public static function findItemUrl($item_type, $item_id) { + $url_config = array( + "product" => "/admin/?opt=product&view=form&id=".$item_id."&part=relation&l=vi&popup=".POPUP, + "product-category" => "?opt=product&view=category-form&id=".$item_id."", + + "article-article" => "?opt=article&view=form&id=".$item_id."&l=&popup=".POPUP, + "article-category" => "?opt=article&view=category-form&id=".$item_id."&l=&popup=".POPUP, + + "album" => "?opt=album&view=form&id=".$item_id, + "banner" => "?opt=banner&view=upload&id=".$item_id, + "page" => "?opt=page&view=form&id=".$item_id, + ); + + return (isset($url_config[$item_type])) ? $url_config[$item_type] : null; + } + + +} diff --git a/inc/Hura8/System/Controller/SettingController.php b/inc/Hura8/System/Controller/SettingController.php new file mode 100644 index 0000000..d1d4e12 --- /dev/null +++ b/inc/Hura8/System/Controller/SettingController.php @@ -0,0 +1,144 @@ +objSettingModel = new SettingModel(); + $this->special_keys = include CONFIG_DIR . "/system/special_settings_keys.php"; + } + + + public function getAll(){ + return $this->objSettingModel->getAll(); + } + + + public function getSpecialKeys() { + $group_keys = $this->special_keys; + return array_merge($group_keys['design'], $group_keys['system']); + } + + + public function get($key, $default =null){ + return $this->objSettingModel->get($key, $default); + } + + + public function delete($key){ + return $this->objSettingModel->delete($key); + } + + + // update bulk key-values + public function updateBulk(array $key_values){ + foreach ($key_values as $key => $value) { + $this->objSettingModel->updateOrCreate($key, $value); + } + } + + + public function updateOrCreate($key, $value, $comment = '') { + return $this->objSettingModel->updateOrCreate($key, $value, $comment ); + } + + + public function getList(array $keys) { + return $this->objSettingModel->getList($keys); + } + + + public function populateSpecialKeys() { + $keys = []; + foreach ($this->special_keys as $group => $_list) { + $keys = array_merge($keys, $_list); + } + + return $this->objSettingModel->populateKeys($keys); + } + + + //tao config file tu database va upload vao thu muc website + // Global config variables available to the whole website + // includes: + /* + * - domain setting + * - main domain + * - template_set + * - exchange rate + * - password to unlock website + * - google analytic verification + * - google webmaster tool verification + * - number of products to display + * - default product display type: list|grid + * + * */ + public function create_config_file_n_upload(){ + + $objDomainController = new DomainController(); + $config_domains = $objDomainController->buildDomainConfig(); + + $config = []; + + // * - domain setting + $config['domains'] = $config_domains; + + // * - template_set + $objATemplateSetController = new ATemplateSetController(); + $config['template_set'] = $objATemplateSetController->getActivatedSet(); + + // * - exchange rate + // * - password to unlock website + // * - google analytic verification + // * - google webmaster tool verification + // * - number of products to display + // * - default product display type: list|grid + $config['setup'] = $this->getList(array( + "web_close_pass" , + "web_close_message" , + "exchange_rate" , + "google_domain_verify" , + "product_per_page" , + "product_default_order", + "site_manager" , + "site_manager_access_key", + )); + + $config_file = CONFIG_DIR . "/build/store.config.php"; + + $config_head = "_minifyCode($config_content)); + } + + + //this is a greatly simplified version + protected function _minifyCode($text) { + //remove line break + $text = str_replace(["\n", "\r", "\t"], " ", $text); + //remove double spacings + $text = preg_replace("/(\s+)/i", " ", $text); + + return trim($text); + } + +} diff --git a/inc/Hura8/System/Controller/UrlManagerController.php b/inc/Hura8/System/Controller/UrlManagerController.php new file mode 100644 index 0000000..dc406b6 --- /dev/null +++ b/inc/Hura8/System/Controller/UrlManagerController.php @@ -0,0 +1,242 @@ +objUrlModel = new UrlModel(); + } + + + public function createRedirect($info) { + $request_path = $info['request_path'] ?? ''; + $redirect_code = $info['redirect_code'] ?? 0; + $redirect_url = $info['redirect_url'] ?? ''; + + if(!$request_path || !$redirect_url) { + return false; + } + + $request_path_element = Url::parse($request_path); + + $request_path_path = $request_path_element['path']; + + // home page or itself is forbidden + if($request_path_path == '/' || $request_path_path == $redirect_url) { + return false; + } + + + return $this->objUrlModel->create([ + "url_type" => "redirect", + "request_path" => $request_path_path, + "redirect_code" => $redirect_code, + "redirect_url" => $redirect_url, + ]); + } + + + public function getInfo($id) : ?array { + return $this->objUrlModel->getInfo($id); + } + + + public function getEmptyInfo($addition_field_value = []) : array { + return $this->objUrlModel->getEmptyInfo($addition_field_value); + } + + + public function getList(array $condition) : array + { + return $this->objUrlModel->getList($condition); + } + + + public function getTotal(array $condition) : int + { + return $this->objUrlModel->getTotal($condition); + } + + + public function deleteByRequestPath($request_path) { + $this->objUrlModel->deleteByRequestPath($request_path); + } + + + public static function translateRequestPathConfig( + $request_path_config, // "/%extra_path%/%item_index%/ac%item_id%.html",/ + $item_id = '', + $item_index = '', + $extra_path = '' + ) { + + $item_index = static::create_url_index($item_index); //reclean url index + + $new_url = str_replace( + array('%item_id%', '%item_index%', '%extra_path%',), + array($item_id, $item_index, $extra_path), + $request_path_config + ); + + return str_replace("//","/",$new_url); + } + + + public static function create_url_index($name, $vietnamese=true){ + if($vietnamese) $name = Language::chuyenKhongdau($name); + + $name = preg_replace("/[^a-z0-9\s_\-]/i", " ", $name); + $name = preg_replace("/\s+/i", " ", $name); + $name = str_replace(" ","-", trim($name)); + $name = preg_replace("/-+/","-", $name); + + if (!defined("BUILD_URL_INDEX_LOWERCASE") || BUILD_URL_INDEX_LOWERCASE) { + return strtolower($name); + } + + return $name; + } + + + public function getUrlMetaInfo($request_path){ + return $this->objUrlModel->getUrlMetaInfo($request_path); + } + + + public function createUrlMeta(array $info){ + return $this->objUrlModel->createUrlMeta($info); + } + + + public function getUrlInfoByRequestPath($request_path) { + $info = $this->objUrlModel->getUrlByRequestPath($request_path); + if($info){ + + $id_path_content = $this->analyze_url_id_path($info['id_path']); + + $id_path_content['option'] = $id_path_content['module']; + //$id_path_content['redirect_code'] = $info['redirect_code']; + //$id_path_content['request_path'] = $info['request_path']; + + return array_merge($info, $id_path_content); + } + + return false; + } + + ///module:product/view:category/view_id:1/brand_id:55/type:hello-ther + //return: + /* + [ + "module" => "product", + "view" => "category", + "view_id" => "1", + 'query' => [ + "brand_id" => 55, + "type" => "hello-ther", + ], + ], + * */ + protected function analyze_url_id_path($id_path) : array + { + $id_path_ele = explode("/", $id_path); + + $result = array(); + $query_string = array(); + + foreach ( $id_path_ele as $ele ) { + if(!$ele) continue; + + $ele_part = explode(":", $ele); + if(!in_array($ele_part[0], array('module', 'view', 'view_id'))) { + $query_string[$ele_part[0]] = $ele_part[1]; + }else{ + $result[$ele_part[0]] = $ele_part[1]; + } + } + + $result['query'] = $query_string; + + return $result; + } + + + public static function createIdPath($module, $view, $view_id, array $other_list = []): string + { + $parameter_constr = [ + "module:".$module, + "view:".$view, + "view_id:".$view_id, + ]; + foreach($other_list as $arg => $value) { + $parameter_constr[] = $arg.":".$value; + } + + return "/".join("/", $parameter_constr); + } + + + /** + * @description create or update url's $request_path by checking $id_path. $new_request_path will return to be updated to item's entity + * @param string $url_type $module:$view + * @param string $wanted_request_path + * @param string $id_path + * @param string $redirect_code + * @return ?string $wanted_request_path or new $request_path if the path already exists + */ + public function createUrl(string $url_type, string $wanted_request_path, string $id_path, $redirect_code=0) : ?string + { + $new_request_path = $this->createUniqueRequestPath($wanted_request_path, $id_path); + + $check_id_path_exist = $this->objUrlModel->getUrlByIdPath($id_path); + if($check_id_path_exist) { + $res = $this->objUrlModel->update($check_id_path_exist['id'], [ + 'request_path' => $new_request_path , + ]); + } else { + $res = $this->objUrlModel->create([ + 'url_type' => $url_type, + 'request_path' => $new_request_path , + 'id_path' => $id_path, + 'redirect_code' => $redirect_code, + ]); + } + + return ($res->getStatus() == AppResponse::SUCCESS) ? $new_request_path : null; + } + + + //create a unique request-path + protected function createUniqueRequestPath(string $wanted_request_path, string $id_path) : string + { + $check_exist = $this->objUrlModel->getUrlByRequestPath($wanted_request_path); + + //ok, can use this one + if(!$check_exist || $check_exist['id_path'] == $id_path ) { + return $wanted_request_path; + } + + //other case, create new path and check again + $random_suffix = IDGenerator::createStringId(4, true); + $new_request_path = $wanted_request_path . '-'.$random_suffix; + + return $this->createUniqueRequestPath($new_request_path, $id_path); + } + + +} diff --git a/inc/Hura8/System/Controller/ViewHistoryController.php b/inc/Hura8/System/Controller/ViewHistoryController.php new file mode 100644 index 0000000..9b5368b --- /dev/null +++ b/inc/Hura8/System/Controller/ViewHistoryController.php @@ -0,0 +1,44 @@ + [id1, id2] + + protected $objWebUserModel; + + public function __construct() + { + $this->objWebUserModel = new WebUserModel(WebUserController::getUserId()); + } + + + public function addHistory($item_type, $item_id) { + $current_list = $this->getHistory($item_type); + + // if exist, remove it + $search_key = array_search($item_id, $current_list, true); + if($search_key !== false) { + array_splice($current_list, $search_key, 1); + } + + // add to front + array_unshift($current_list, $item_id); + + $this->history[$item_type] = $current_list; + + // save to db + $this->objWebUserModel->setValue("view-history", $this->history); + } + + + public function getHistory($item_type) { + $history = $this->objWebUserModel->getValue("view-history"); + return (isset($history[$item_type])) ? $history[$item_type] : []; + } + +} diff --git a/inc/Hura8/System/Controller/WebUserController.php b/inc/Hura8/System/Controller/WebUserController.php new file mode 100644 index 0000000..fffe2f6 --- /dev/null +++ b/inc/Hura8/System/Controller/WebUserController.php @@ -0,0 +1,27 @@ +objWebUserModel = new WebUserModel(self::getUserId()); + } + + // prefer cookie-id, if set ID in url parameter then use it + public static function getUserId() { + $url_user_id = getRequest("uid", ""); + $cookie_user_id = (isset($_COOKIE[self::USER_BROWSER_COOKIE_NAME])) ? $_COOKIE[self::USER_BROWSER_COOKIE_NAME] : ''; + + return ($url_user_id) ?: $cookie_user_id; + } + + +} diff --git a/inc/Hura8/System/Controller/aAdminEntityBaseController.php b/inc/Hura8/System/Controller/aAdminEntityBaseController.php new file mode 100644 index 0000000..d87b6ee --- /dev/null +++ b/inc/Hura8/System/Controller/aAdminEntityBaseController.php @@ -0,0 +1,78 @@ +iEntityModel->updateFields($id, $info); + } + + + public function updateField($id, $field, $value): AppResponse + { + + $fields_list = []; + $fields_list[$field] = $value; + + return $this->iEntityModel->updateFields($id, $fields_list); + } + + + public function updateFeatured($id, $new_status): AppResponse + { + $status = ($new_status == 'on' || $new_status == 1) ? 1 : 0; + + return $this->iEntityModel->updateFields($id, ['is_featured' => $status]); + } + + + public function updateStatus($id, $new_status): AppResponse + { + $status = ($new_status == 'on' || $new_status == 1) ? 1 : 0; + + return $this->iEntityModel->updateFields($id, ['status' => $status]); + } + + + public function create(array $info): AppResponse + { + return $this->iEntityModel->create($info); + } + + + public function update($id, array $info): AppResponse + { + if($this->iEntityLanguageModel) { + return $this->iEntityLanguageModel->update($id, $info); + } + + return $this->iEntityModel->update($id, $info); + } + + + abstract protected function deleteFileBeforeDeleteItem($item_id) : bool; + + + public function delete($id): AppResponse + { + if($this->deleteFileBeforeDeleteItem($id)) { + return $this->iEntityModel->delete($id); + } + + return new AppResponse('error', 'Cannot delete '.$id); + } + + + public function getEmptyInfo(array $additional_fields = []) : array + { + return $this->iEntityModel->getEmptyInfo($additional_fields); + } + + +} diff --git a/inc/Hura8/System/Controller/aCategoryBaseController.php b/inc/Hura8/System/Controller/aCategoryBaseController.php new file mode 100644 index 0000000..333b7d2 --- /dev/null +++ b/inc/Hura8/System/Controller/aCategoryBaseController.php @@ -0,0 +1,304 @@ +iEntityCategoryModel = $iEntityCategoryModel; + + if(!$this->isDefaultLanguage() && $iEntityLanguageModel instanceof iEntityLanguageModel) { + $this->iEntityLanguageModel = $iEntityLanguageModel; + $this->iEntityLanguageModel->setLanguage($this->view_language); + } + } + + + public function create(array $info) : AppResponse + { + return $this->iEntityCategoryModel->create($info); + } + + + public function update($id, array $info) : AppResponse + { + if($this->iEntityLanguageModel) { + return $this->iEntityLanguageModel->update($id, $info, $info['title']); + } + + return $this->iEntityCategoryModel->update($id, $info); + } + + + public function delete($id) : AppResponse + { + return $this->iEntityCategoryModel->delete($id); + } + + + public function updateFields($id, array $info) : AppResponse + { + return $this->iEntityCategoryModel->updateFields($id, $info); + } + + + public function isDefaultLanguage() : bool + { + return IS_DEFAULT_LANGUAGE; + } + + + public function getListByIds(array $list_id, array $condition = array()) : array + { + $item_list = $this->iEntityCategoryModel->getListByIds($list_id, $condition); + + if($this->iEntityLanguageModel) { + $item_list_language_info = $this->iEntityLanguageModel->getListByIds($list_id); + + $final_list = []; + foreach ($item_list as $item) { + $item_language_info = $item_list_language_info[$item['id']] ?? ["not_translated" => true]; + $final_list[] = $this->formatItemInList(array_merge($item, $item_language_info)); + } + + return $final_list; + } + + return array_map(function ($item){ + return $this->formatItemInList($item); + }, $item_list); + } + + + protected function formatItemInList(array $info): array + { + return $info; + } + + + public function getActualFilterCondition(array $raw_filter_condition) : array + { + return $raw_filter_condition; + } + + + public function getModelFilterCondition(array $raw_filter_condition) : array + { + return $this->iEntityCategoryModel->getQueryCondition( $raw_filter_condition); + } + + + public function getList(array $condition) : array + { + $item_list = $this->iEntityCategoryModel->getList($condition); + + if($this->iEntityLanguageModel) { + $list_ids = array_map(function ($item){ return $item['id'];}, $item_list); + $item_list_language_info = $this->iEntityLanguageModel->getListByIds($list_ids); + + $final_list = []; + foreach ($item_list as $item) { + $item_language_info = $item_list_language_info[$item['id']] ?? ["not_translated" => true]; + $final_list[] = $this->formatItemInList(array_merge($item, $item_language_info)); + } + + return $final_list; + } + + return array_map(function ($item){ + return $this->formatItemInList($item); + }, $item_list); + } + + + public function getTotal(array $condition) : int + { + return $this->iEntityCategoryModel->getTotal($condition); + } + + + protected function formatItemInfo(?array $info) : ?array + { + return $info; + } + + + public function getInfo($id) : ?array + { + if(!$id) return null; + + return self::getCache("getInfo-".$id."-".$this->view_language, function () use ($id){ + + if($this->iEntityLanguageModel) { + $info = $this->iEntityCategoryModel->getInfo($id); + $item_language_info = $this->iEntityLanguageModel->getInfo($id); + if($item_language_info) { + return $this->formatItemInfo(array_merge($info, $item_language_info)); + } + } + + return $this->formatItemInfo($this->iEntityCategoryModel->getInfo($id)); + + }); + } + + + public function getEmptyInfo(array $additional_fields = []) : array + { + return $this->iEntityCategoryModel->getEmptyInfo($additional_fields); + } + + + // all categories and nested by parent + public function getNestedCategories($is_public = false) { + + $cache_key = "getNestedCategories-".$this->iEntityCategoryModel->getEntityType()."-".($is_public ? 1: 0); + + return self::getCache($cache_key, function () use ($is_public){ + $all_categories = $this->getAllParent([ + 'status' => ($is_public) ? 1 : 0 + ]); + + $result = []; + if(isset($all_categories[0])) { + foreach ($all_categories[0] as $index => $info) { + $info['children'] = ($info['is_parent']) ? $this->getChildren($info['id'], $all_categories, 1) : []; + $result[] = $this->formatItemInList($info); + } + } + + return $result; + }); + + } + + + public function getChildren($parent_id, $all_categories, $current_level = 1){ + // dont allow too many nested level + $max_deep_level_allow = 5; + if($current_level == $max_deep_level_allow) return []; + + $result = []; + if(isset($all_categories[$parent_id])) { + foreach ($all_categories[$parent_id] as $id => $info) { + $info['children'] = $this->getChildren($id, $all_categories, $current_level + 1); + $result[] = $this->formatItemInList($info); + } + } + + return $result; + } + + + public function showPath($catPathId, $p_category){ + + if(!$catPathId){ + $cat_info = $this->getInfo($p_category); + $catPathId = $cat_info['cat_path']; + } + + $result = array(); + $path_build = array(); + + if(strlen($catPathId) > 0){ + $cat_id_list = array_filter(explode(":", $catPathId)); + + $cat_list_info = $this->getListByIds($cat_id_list); + + //reverse cat id order because the parent in the right most + krsort($cat_id_list); + + $start_count = 0; + $count_cat = sizeof($cat_id_list); + + foreach($cat_id_list as $catId){ + + $_cat_info = $cat_list_info[$catId] ?? null; + if(!$_cat_info) continue; + + $start_count ++; + $path_build[] = "".$_cat_info['title']." "; + + $result[] = array( + 'id' => $catId, + 'url' => $_cat_info['url'], + 'title' => $_cat_info['title'], + ); + + if($start_count < $count_cat) $path_build[] = " >> "; + } + } + + return array( + 'path' => $result, + 'path_url' => join("", $path_build), + ); + } + + + public function getAll(array $condition = []) { + return $this->iEntityCategoryModel->getAll($condition); + } + + + public function getAllParent(array $condition = []) { + + if($this->iEntityLanguageModel) { + + $all_categories = $this->getAll($condition); + + $item_list_ids = array_map(function ($item){ return $item['id']; }, $all_categories); + $item_list_language_info = $this->iEntityLanguageModel->getListByIds($item_list_ids); + + $translated_list = []; + foreach ($all_categories as $item) { + $item_language_info = $item_list_language_info[$item['id']] ?? ["not_translated" => true]; + $item = array_merge($item, $item_language_info); + + $translated_list[] = array_merge($item, $item_language_info); + } + + // get group by parent + $final_list = []; + foreach ( $translated_list as $item ) { + $final_list[$item['parent_id']][$item['id']] = $this->formatItemInList($item); + } + + return $final_list; + } + + // else + $all_categories = $this->getAll($condition); + $final_list = []; + foreach ( $all_categories as $item ) { + $final_list[$item['parent_id']][$item['id']] = $this->formatItemInList($item); + } + + return $final_list; + } + + +} diff --git a/inc/Hura8/System/Controller/aERPController.php b/inc/Hura8/System/Controller/aERPController.php new file mode 100644 index 0000000..3575b90 --- /dev/null +++ b/inc/Hura8/System/Controller/aERPController.php @@ -0,0 +1,197 @@ +provider = $provider; + + $provider_config_file = CONFIG_DIR . '/provider/'.$provider.'_config.php'; + if(file_exists($provider_config_file)) { + $this->erp_config = include $provider_config_file; + + // create an instance of provider + $this->erpFactory(); + + $this->db = ConnectDB::getInstance(''); + }else{ + die("Cannot load /config/provider/".$provider."_config.php "); + } + } + + abstract protected function customSyncProductToWeb(); + abstract protected function formatProductListFromERP(array $product_list); + + /** + * @overwrite on implemeting class if needed + */ + public function getProductListPerPage($page, $debug = false) { + return $this->getProductList(['page' => $page], $debug); + } + + /** + * @return iERPProvider + */ + public function getERPInstance() + { + return $this->objERPProvider; + } + + public function createOrder(array $order_info) + { + return $this->objERPProvider->createOrder($order_info); + } + + /** + * @param array $options + */ + public function syncProductToWeb(array $options = []) + { + $this->beforeSyncProductToWeb(); + $this->customSyncProductToWeb(); + $this->afterSyncProductToWeb(); + } + + protected function beforeSyncProductToWeb() { + /*$this->db->runQuery("CREATE TABLE `" . $this->tb_erp_product_copy . "` LIKE ".$this->tb_erp_product." "); + $this->db->runQuery("INSERT INTO `" . $this->tb_erp_product_copy . "` SELECT * FROM ".$this->tb_erp_product." "); + $this->db->runQuery("UPDATE `" . $this->tb_erp_product_copy . "` e, idv_sell_product_store p SET + e.product_id = p.id + WHERE e.sku = p.storeSKU AND LENGTH(e.sku) > 0 "); + $this->db->runQuery("DELETE FROM `" . $this->tb_erp_product_copy . "` WHERE `product_id` = 0 ");*/ + + $this->db->multi_query([ + "CREATE TABLE IF NOT EXISTS `" . $this->tb_erp_product_copy . "` LIKE ".$this->tb_erp_product." ", + "INSERT INTO `" . $this->tb_erp_product_copy . "` SELECT * FROM ".$this->tb_erp_product." ", + "UPDATE `" . $this->tb_erp_product_copy . "` e, ".$this->tb_product." p SET + e.product_id = p.id + WHERE e.sku = p.storeSKU AND LENGTH(e.sku) > 0 ", + ]); + + } + + protected function afterSyncProductToWeb() { + $this->db->runQuery("DROP TABLE `" . $this->tb_erp_product_copy . "` "); + } + + public function setERPProductOptions(array $options = []) { + foreach ($options as $key => $value) { + $this->get_erp_product_options[$key] = $value; + } + } + + /** + * @description: clean any existing data before populate new ones + * @return void + */ + public function cleanExistingData() + { + $this->db->runQuery("TRUNCATE `" . $this->tb_erp_product . "` "); + } + + public function getAllStore() + { + return $this->objERPProvider->getAllStore(); + } + + + public function getProductSummary() + { + return $this->objERPProvider->getProductSummary(); + } + + + public function getProductList(array $options = [], $debug = false) + { + return $this->formatProductListFromERP($this->objERPProvider->getProductList($options, $debug)); + } + + + /** + * get log data + */ + public function getLog($type, $limit = 50) { + $query = $this->db->runQuery("SELECT `id`, `data`, `log_time` FROM `".$this->tb_log."` WHERE `type` = ? ORDER BY `id` DESC LIMIT ".$limit, ['s'], [$type]); + + return array_map(function ($item){ + $copy = $item; + $copy['data'] = $item['data'] ? \json_decode($item['data'], true) : []; + + return $copy; + }, $this->db->fetchAll($query)); + } + + /** + * log data + */ + public function updateLogData($id, $new_data) { + + $query = $this->db->runQuery("SELECT `data` FROM `".$this->tb_log."` WHERE `id` = ? LIMIT 1", ['d'], [$id]); + + if($item_info = $this->db->fetchAssoc($query)) { + $current_data = $item_info['data'] ? \json_decode($item_info['data'], true) : []; + $updated_info = array_merge($current_data, $new_data); + + return $this->db->update($this->tb_log, ['data' => $updated_info], ['id' => $id]); + } + + return false; + } + + /** + * log data + */ + public function log($type, array $data) { + $info = []; + $info['type'] = $type; + $info['data'] = $data; + $info['log_time'] = CURRENT_TIME; + + return $this->db->insert($this->tb_log, $info); + } + + protected function erpFactory() + { + if(!$this->provider) { + die("No provider found!"); + } + + $provider_class = 'Provider\\ERPProviders\\'.ucfirst($this->provider); + + try { + + $this->objERPProvider = (new \ReflectionClass($provider_class))->newInstance($this->erp_config); + + } catch (\ReflectionException $e) { + die("aClientERP/erpFactory: ".$e->getMessage()); + } + } + +} diff --git a/inc/Hura8/System/Controller/aEntityBaseController.php b/inc/Hura8/System/Controller/aEntityBaseController.php new file mode 100644 index 0000000..44ad562 --- /dev/null +++ b/inc/Hura8/System/Controller/aEntityBaseController.php @@ -0,0 +1,262 @@ +iEntityModel = $iEntityModel; + + if(!$this->isDefaultLanguage() && $iEntityLanguageModel instanceof iEntityLanguageModel) { + $this->iEntityLanguageModel = $iEntityLanguageModel; + + // only controller allow to control the language for the model + $this->iEntityLanguageModel->setLanguage($this->view_language); + } + } + + + public function getLanguage() { + return $this->view_language; + } + + + public function isDefaultLanguage() { + return IS_DEFAULT_LANGUAGE; + } + + + public function getFilterConditions() : array { + return array_merge( + $this->baseFilterConditions(), + $this->extendFilterConditions() + ); + } + + + // this is supposed to be overwritten by the extending class if required + protected function extendFilterConditions() : array { + return []; + } + + // just here to show the reserved keys. That's all + protected function baseFilterConditions() { + $base_filter = [ + 'q' => getRequest("q", ''), // keyword search + 'q_options' => [ + 'field_filters' => [], + 'fulltext_fields' => [], + 'limit_result' => 2000 + ], // q_options as in iSearch->find($keyword, array $field_filters = [], array $fulltext_fields = ["keywords"], $limit_result = 2000) + 'featured' => getRequestInt("featured", 0), // 1|-1 + 'status' => getRequestInt("status", 0), // 1|-1 + 'excluded_ids' => null, // [id1, id2, ...] + 'included_ids' => null,// [id1, id2, ...] + + // to special filters for language + 'translated' => getRequestInt("translated", 0), // 1|-1 + + // for sorting + 'sort_by' => getRequest('sort', ''), + + // for pagination, not exactly for filter but put here to reserve the keys + 'numPerPage' => getRequestInt("show", 20), + 'page' => getPageId(), + ]; + + if(getRequest("excluded_ids", '') != '') { + $base_filter['excluded_ids'] = explode("-", getRequest("excluded_ids", '')); + } + + if(getRequest("included_ids", '') != '') { + $base_filter['included_ids'] = explode("-", getRequest("included_ids", '')); + } + + return $base_filter; + } + + + protected function formatItemInList(array $item_info) + { + return $item_info; + } + + + protected function formatItemInfo(array $item_info) + { + return $item_info; + } + + + public function getListByIds(array $list_id, array $condition = array()) : array + { + $item_list = array_map(function ($item){ + return $this->formatItemInList($item); + }, $this->iEntityModel->getListByIds($list_id, $condition)); + + if($this->iEntityLanguageModel) { + $item_list_language_info = $this->iEntityLanguageModel->getListByIds($list_id); + + $final_list = []; + foreach ($item_list as $item) { + $item_language_info = $item_list_language_info[$item['id']] ?? ["not_translated" => true]; + $final_list[] = array_merge($item, $item_language_info); + } + + return $final_list; + } + + return $item_list; + } + + + // for extending controller class to validate and clean data in the $raw_filter_condition (ie. from URL) + // before sending to model for data querying + // extending controller must overwrite this + protected function validateAndCleanFilterCondition(array $raw_filter_condition) : array { + + $clean_values = []; + + foreach ($raw_filter_condition as $key => $value) { + // default + if(is_array($value)) { + $clean_values[$key] = DataClean::makeListOfInputSafe($value, DataType::ID); + }else{ + $clean_values[$key] = DataClean::makeInputSafe($value, DataType::ID); + } + } + + return $clean_values; + } + + + /** + * @description utility to inspect the actual filters which will be used in getList + * make sure to edit the ::_buildQueryConditionExtend method on the Model so the Model will parse the filters provided by controller here + * @param array $raw_filter_condition + * @return string[] + */ + public function getActualFilterCondition(array $raw_filter_condition) : array + { + return $this->buildFilterQuery($raw_filter_condition); + } + + /** + * @description utility to inspect the actual filters which will be used in getList by Model + * @param array $raw_filter_condition + * @return string[] + */ + public function getModelFilterCondition(array $raw_filter_condition) : array + { + return $this->iEntityModel->getQueryCondition($this->buildFilterQuery($raw_filter_condition)); + } + + + public function getList(array $raw_filter_condition) : array + { + + $filter_condition = $this->buildFilterQuery($raw_filter_condition); + //debug_var($filter_condition); + + $item_list = array_map(function ($item){ + return $this->formatItemInList($item); + }, $this->iEntityModel->getList($filter_condition)); + + + if($this->iEntityLanguageModel) { + + $item_list_ids = array_map(function ($item){ return $item['id']; }, $item_list); + $item_list_language_info = $this->iEntityLanguageModel->getListByIds($item_list_ids); + + $final_list = []; + foreach ($item_list as $item) { + $item_language_info = $item_list_language_info[$item['id']] ?? ["not_translated" => true]; + $final_list[] = array_merge($item, $item_language_info); + } + + return $final_list; + } + + return $item_list; + } + + + public function getTotal(array $raw_filter_condition) : int + { + $filter_condition = $this->buildFilterQuery($raw_filter_condition); + + return $this->iEntityModel->getTotal($filter_condition); + } + + + protected function buildFilterQuery(array $raw_filter_condition) : array + { + $filter_condition = $this->validateAndCleanFilterCondition($raw_filter_condition); + + // special case to filter out which ids have not been translated + if(isset($filter_condition['translated']) && $filter_condition['translated'] && $this->iEntityLanguageModel) { + if($filter_condition['translated'] == 1) { + $filter_condition['included_ids'] = $this->iEntityLanguageModel->getTranslatedIds(); + }else{ + $filter_condition['excluded_ids'] = $this->iEntityLanguageModel->getTranslatedIds(); + } + } + + return $filter_condition; + } + + + public function getInfo($id): ?array + { + if(!$id) return null; + + return self::getCache("getInfo-".$id."-".$this->view_language, function () use ($id){ + + $info = $this->iEntityModel->getInfo($id); + + if($this->iEntityLanguageModel && $info) { + $item_language_info = $this->iEntityLanguageModel->getInfo($id); + //debug_var($item_language_info); + + if($item_language_info) { + return $this->formatItemInfo(array_merge($info, $item_language_info)); + }else{ + $info["not_translated"] = true; + + return $this->formatItemInfo($info); + } + } + + return ($info) ? $this->formatItemInfo($info) : null; + + }); + + } + + +} diff --git a/inc/Hura8/System/Controller/aExcelDownloadController.php b/inc/Hura8/System/Controller/aExcelDownloadController.php new file mode 100644 index 0000000..d462c03 --- /dev/null +++ b/inc/Hura8/System/Controller/aExcelDownloadController.php @@ -0,0 +1,344 @@ + array( + 'name' => 'ID Sản phẩm Web', + 'width' => '10', + 'data_field_name' => 'id', + ), + "B" => array( + 'name' => 'Mã kho (SKU)', + 'width' => '10', + 'data_field_name' => 'storeSKU', + ),*/ + //... + ]; + + protected $field_column_mappings = [ + //'name' => "A", + //'price' => "B", + ]; + + private $header_row_index = 2; + + // hold cach for some operations + protected $cache = []; + + protected $format_item_middlewares = []; // list of middleware object to format item + + + public function __construct($client_config_file_name='', $export_file_name='', $work_sheet_title = '') + { + if($client_config_file_name) { + $this->setColumnConfigUseConfigFile($client_config_file_name); + } + + $this->export_file_name = ($export_file_name) ?: "file_".CURRENT_TIME; + $this->work_sheet_title = ($work_sheet_title) ?: "Danh sách"; + } + + + protected function setColumnConfigUseConfigFile($client_config_file_name) { + //this from a config file for each client + $client_config_file = "config/client/excel/".$client_config_file_name; + if( ! file_exists(ROOT_DIR .'/'. $client_config_file)) { + die("Please create config file: ".$client_config_file); + } + $client_fields_config = include ROOT_DIR .'/'. $client_config_file; + + // auto add excel column names based on fields' index + $this->client_excel_col_config = $this->_make_columns(array_values($client_fields_config)); + + // create field-col map + $this->createFieldColumnMappings(); + } + + + private function createFieldColumnMappings() { + $field_column_mappings = []; + foreach ($this->client_excel_col_config as $column_name => $_prop) { + $field_column_mappings[$_prop['data_field_name']] = $column_name; + } + $this->field_column_mappings = $field_column_mappings; + } + + + public function setColumnConfigManually(array $col_config) { + $this->client_excel_col_config = $this->_make_columns($col_config); + // create field-col map + $this->createFieldColumnMappings(); + } + + + protected function _make_columns($fields_config) { + $new_array = []; + $total_names = sizeof(static::$column_names); + foreach ($fields_config as $index => $config) { + if($index >= $total_names) break; + $new_array[static::$column_names[$index]] = $config; + } + + return $new_array; + } + + + public function start(array $options = [ + "debug_mode" => '', // show-item-list + "excelOption" => [], + "sheetOption" => [], + "sheetStartRowNumber" => 3, + "sheetHeaderRowNumber" => 2, + "getItemListOption" => [ + "brand" => [], + "category" => [], + "page" => 1, + "limit" => 100, + ], + "exportFileOption" => [], + ]) { + + $debug_mode = $options['debug_mode'] ?? false; + // debug mode + if($debug_mode) { + // show item list + if($debug_mode == 'show-item-list') { + $item_list = $this->getItemList($options['getItemListOption']); + print_r($item_list); + } + + // show formatted list + if($debug_mode == 'show-formatted-list') { + $item_list = $this->formatItemList( $this->getItemList($options['getItemListOption']) ); + print_r($item_list); + } + + return true; + } + + // setup + if(isset($options['sheetHeaderRowNumber']) && $options['sheetHeaderRowNumber']) { + $this->header_row_index = $options['sheetHeaderRowNumber']; + } + + $this->createExcelObject($options['excelOption'] ?? []); + $this->createActiveSheet(0, $options['sheetOption'] ?? []); + + // num beforeWriteList + $this->beforeWriteList(); + + // fetch all items and write till the end + $has_fetch_all_items = false; + $start_row = (isset($options["sheetStartRowNumber"])) ? $options["sheetStartRowNumber"] : 3; + + $item_list = $this->formatItemList( $this->getItemList($options['getItemListOption']) ); + + //debug_var($item_list); + //exit; + + $has_fetch_all_items = true; + $this->writeItemsToExcel($start_row, $item_list); + + /*$getItemListOption = $options['getItemListOption']; + $pageIndex = 0; + + while (!$has_fetch_all_items) { + // run from page 1->end + $pageIndex += 1; + $getItemListOption["page"] = $pageIndex; + $item_list = $this->getItemList($getItemListOption); + + // flag + if(!sizeof($item_list)) { + $has_fetch_all_items = true; + } + + // else, start write + $last_row = $this->writeItemsToExcel($start_row, $item_list); + + // update $start_row + $start_row = $last_row;// + 1 + }*/ + + // export + if($has_fetch_all_items) { + $this->getExcelFile($options['exportFileOption']); + } + + return true; + } + + // abstract methods + protected function beforeWriteList() { } + abstract protected function getItemList(array $options); + abstract protected function defaultFormatItemInfo(array $item_info, $index =0); + + protected function registerFormatItemInfoMiddleware($middleware_ojb) { + $this->format_item_middlewares[] = $middleware_ojb; + } + + protected function formatItemInfo(array $item_info, $index = 0) { + + // apply middleware + if(sizeof($this->format_item_middlewares)) { + foreach ($this->format_item_middlewares as $_middleware) { + $item_info = call_user_func($_middleware, $item_info); + } + } else { + $item_info = $this->defaultFormatItemInfo($item_info, $index); + } + + return $item_info; + } + + + protected function createExcelObject(array $options) + { + // Create new Spreadsheet object + $this->objExcel = new Spreadsheet(); + + // Set properties + $this->objExcel->getProperties()->setCreator("Hurasoft"); + $this->objExcel->getProperties()->setLastModifiedBy("Hurasoft"); + $this->objExcel->getProperties()->setTitle("Excel Document"); + $this->objExcel->getProperties()->setSubject("Excel Document"); + $this->objExcel->getProperties()->setDescription("Tao file excel"); + } + + + protected function createActiveSheet($sheet_index = 0, array $options=[]) { + // Create a first sheet, representing sales data + $this->objExcel->setActiveSheetIndex($sheet_index); + $this->currentActiveSheet = $this->objExcel->getActiveSheet(); + $this->currentActiveSheet->setCellValueExplicit('A1', $this->work_sheet_title, DataType::TYPE_STRING); + $this->currentActiveSheet->getStyle('A1')->getFont()->setSize(16); + $this->currentActiveSheet->getStyle('A1')->getFont()->setBold(true); + + // Set header row + $row_index = $this->header_row_index; + foreach ($this->client_excel_col_config as $col_name => $_prop) { + $col_width = (isset($_prop['width']) && intval($_prop['width']) > 0) ? intval($_prop['width']) : 15; + $this->currentActiveSheet->getColumnDimension($col_name)->setWidth($col_width); + $this->currentActiveSheet->setCellValueExplicit($col_name. $row_index, $_prop["name"], DataType::TYPE_STRING); + $this->currentActiveSheet->getStyle($col_name . $row_index)->getFont()->setBold(true); + } + } + + protected function formatItemList(array $item_list) + { + $new_list = []; + foreach ( $item_list as $index => $item_info ) { + $new_list[$index] = $this->formatItemInfo($item_info, $index); + } + + return $new_list; + } + + + protected function writeItemsToExcel($start_row = 1, array $item_list=[]) + { + $write_row = $start_row; + foreach ( $item_list as $index => $item_info ) { + + // write each field to its corresponding columns + foreach ($item_info as $_field => $_value) { + if( !isset($this->field_column_mappings[$_field])) continue; + + if(is_array($_value)) $_value = serialize($_value); + + $write_column = $this->field_column_mappings[$_field]; + + $this->currentActiveSheet->setCellValueExplicit($write_column . $write_row, $_value, DataType::TYPE_STRING); + $this->currentActiveSheet->getStyle($write_column . $write_row)->getAlignment()->setWrapText(true); + } + + // next rows + $write_row += 1; + } + + // get the last row + return $write_row; + } + + + protected function getExcelFile(array $options) + { + // write to a local file + $local_file = $options['local_file'] ?? ''; + if($local_file) { + $this->_save_to_file($local_file); + return true; + } + + // default: export to browser to download + $this->_export_to_browser(); + + return true; + } + + protected function cleanUp(){ + // clean up + $this->objExcel->disconnectWorksheets(); + unset($this->objExcel); + } + + protected function _save_to_file($file_path){ + + // delete old file if exist + if(file_exists($file_path)) { + @unlink($file_path); + } + + $writer = new Xlsx($this->objExcel); + $writer->save($file_path); + $this->cleanUp(); + } + + protected function _export_to_browser(){ + // Rename sheet + header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + header('Content-Disposition: attachment;filename="'.$this->export_file_name.'.xlsx"'); + header('Cache-Control: max-age=0'); + + $writer = new Xlsx($this->objExcel); + ob_end_clean(); + $writer->save('php://output'); + + $this->cleanUp(); + exit(); + } + +} diff --git a/inc/Hura8/System/Controller/aExcelUploadController.php b/inc/Hura8/System/Controller/aExcelUploadController.php new file mode 100644 index 0000000..5f545e2 --- /dev/null +++ b/inc/Hura8/System/Controller/aExcelUploadController.php @@ -0,0 +1,355 @@ + array( + 'name' => 'ID Sản phẩm Web', + 'width' => '10', + 'data_field_name' => 'id', + ), + "B" => array( + 'name' => 'Mã kho (SKU)', + 'width' => '10', + 'data_field_name' => 'storeSKU', + ),*/ + //... + ]; + + protected $field_column_mappings = [ + //'name' => "A", + //'price' => "B", + ]; + + // hold cache for some operations + protected $cache = []; + + protected $format_item_middlewares = []; // list of middleware object to format item + + + public function __construct($client_config_file_name = '', $file_input_name = '', array $update_option = []) + { + + if(!$client_config_file_name) { + return true; + } + + //this from a config file for each client + $client_config_file = "config/client/excel/" . $client_config_file_name; + if (!file_exists(ROOT_DIR . '/' . $client_config_file)) { + die("Please create config file: " . $client_config_file); + } + $client_fields_config = include ROOT_DIR . '/' . $client_config_file; + + if($file_input_name) { + $this->file_input_name = $file_input_name; + } + + // auto add excel column names based on fields' index + $this->client_excel_col_config = $this->_make_columns(array_values($client_fields_config)); + + // create field-col map + $field_column_mappings = []; + foreach ($this->client_excel_col_config as $column_name => $_prop) { + if(!$_prop['data_field_name']) continue; + + // skip column which is not for upload + if(isset($_prop['for_upload']) && !$_prop['for_upload']) continue; + + $field_column_mappings[$_prop['data_field_name']] = $column_name; + } + + $this->field_column_mappings = $field_column_mappings; + + $this->update_option = $update_option; + + return true; + } + + + public function updateUpdateOptions(array $update_option = []) { + foreach ($update_option as $key => $value) { + $this->update_option[$key] = $value; + } + } + + + public function getColumnNotForUpload() { + $result = []; + foreach ($this->client_excel_col_config as $column_name => $_prop) { + // skip column which is not for upload + if(isset($_prop['for_upload']) && !$_prop['for_upload']) $result[$column_name] = $_prop['name'] ; + } + + return $result; + } + + + public function getColumnCanUpdate() { + $result = []; + foreach ($this->client_excel_col_config as $column_name => $_prop) { + // skip column which is not for upload + if(isset($_prop['for_upload']) && !$_prop['for_upload']) continue; + + // skip column which is not for update + if(isset($_prop['can_update']) && !$_prop['can_update']) continue; + + $result[] = $_prop ; + } + + return $result; + } + + + protected function getRequiredFields() { + $result = []; + foreach ($this->client_excel_col_config as $column_name => $_prop) { + // skip column which is not for upload + if(isset($_prop['required']) && $_prop['required']) $result[] = $_prop['data_field_name'] ; + } + + return $result; + } + + + protected function _make_columns($fields_config) + { + $new_array = []; + $total_names = sizeof(static::$column_names); + foreach ($fields_config as $index => $config) { + if ($index >= $total_names) break; + $new_array[static::$column_names[$index]] = $config; + } + + return $new_array; + } + + + protected function getExcelFileExt($excel_file){ + $ext = substr(strrchr($excel_file, "."), 1); + return (in_array($ext, ["xls", "xlsx"])) ? $ext : false; + } + + + public function start($sheet_start_row = 3, $batch_mode = false, $batch_size=100) + { + $ext = $this->getExcelFileExt($_FILES[$this->file_input_name]["name"]); + if(!$ext) { + return [ + 'status' => 'error', + 'message' => 'Invalid excel file', + ]; + } + + $objReadExcel = new ReadExcel($ext); + + $all_rows = $objReadExcel->read( + $_FILES[$this->file_input_name]["tmp_name"], + $sheet_start_row, + $this->field_column_mappings, + '', + true + ); + + $this->beforeProcessRows($all_rows); + + $success_row_counter = 0; + + if($batch_mode) { + // batch mode + $small_batch = []; + $counter = 0; + foreach ($all_rows as $sheet_index => $sheet_content) { + foreach ($sheet_content as $row_id => $row_content) { + + $formatted_info = $this->formatItemInfo($this->convertColToField($row_content)); + if(!$formatted_info) continue; + + $counter += 1; + + $small_batch[] = $formatted_info; + if($counter % $batch_size == 0) { + $success_row_counter += $this->processBatchItems($small_batch); + // reset + $counter = 0; + $small_batch = []; + } + } + } + + // process the remain + if(sizeof($small_batch)) { + $success_row_counter += $this->processBatchItems($small_batch); + } + + } else { + // single item mode + foreach ($all_rows as $sheet_index => $sheet_content) { + foreach ($sheet_content as $row_index => $row_content) { + + $formatted_info = $this->formatItemInfo($this->convertColToField($row_content)); + if(!$formatted_info) continue; + + if($this->processItem($formatted_info, $row_index)){ + $success_row_counter += 1; + } + } + } + } + + //unset($all_rows); + + return [ + 'status' => 'success', + 'message' => '', + 'success_counter' => $success_row_counter, + ]; + } + + + protected function convertExcelDateValue($value) { + // Date value in Excel's cell can be displayed as text or date value, we need to check for both + $format_value = $this->formatExcelUploadDate($value); + + if(!$format_value) { + // check if it's excel date + try { + $format_value = (is_numeric($value)) ? date("d-m-Y", Date::excelToTimestamp($value, date_default_timezone_get())) : ''; + }catch (\Exception $e) { + $format_value = ''; + } + } + + return $format_value; + } + + + protected function formatExcelUploadDate($input_date) { + $check_date_pattern = "/\d{1,2}-\d{1,2}-\d{4}/i"; + $format_date = str_replace("/", "-", $input_date); + + if(preg_match($check_date_pattern, $format_date)) { + return date("d-m-Y", strtotime($format_date)); + } + + return null; + } + + protected function convertExcelHourMinuteValue($value) { + $format_value = $this->formatExcelUploadHourMinute($value); + + if(!$format_value) { + // check if it's excel date + try { + $format_value = (is_numeric($value)) ? date("H:i", Date::excelToTimestamp($value, date_default_timezone_get())) : ''; + }catch (\Exception $e) { + $format_value = ''; + } + } + + return $format_value; + } + + + protected function formatExcelUploadHourMinute($input_date) { + $check_date_pattern = "/\d{1,2}:\d{1,2}/i"; + //$format_date = str_replace("/", "-", $input_date); + + if(preg_match($check_date_pattern, $input_date)) { + return date("H:i", strtotime($input_date)); + } + + return null; + } + + // ['A' => '12', 'B' => 'ten sp'] => ['id' => '12', 'product_name' => 'ten sp'] + protected function convertColToField(array $row_content ) { + + if(!$this->field_column_mappings) { + return array_values($row_content); + } + + $item_info = []; + foreach ($this->field_column_mappings as $field => $col) { + //if(!isset($row_content[$col])) continue; + $item_info[$field] = $row_content[$col]; + } + + return $item_info; + } + + // abstract methods + protected function beforeProcessRows(array &$all_read_rows){ + // default nothing + // derived class should overwrite this method + } + abstract protected function processItem(array $item_info, $row_index=0); + abstract protected function processBatchItems(array $item_list); + + /** + * @param array $item_info + * @return array|null + */ + abstract protected function formatItemInfoBeforeProcess(array $item_info); + + public function registerFormatItemInfoMiddleware($middleware_ojb) + { + $this->format_item_middlewares[] = $middleware_ojb; + } + + + protected $date_fields = []; + public function setDateFields(array $date_fields) { + $this->date_fields = $date_fields; + } + + protected $hour_minute_fields = []; + public function setHourMinuteFields(array $hour_minute_fields) { + $this->hour_minute_fields = $hour_minute_fields; + } + + /** + * @param array $item_info + * @return array|null + */ + protected function formatItemInfo(array $item_info) + { + $copy = $item_info; + + // apply middleware + if (sizeof($this->format_item_middlewares)) { + foreach ($this->format_item_middlewares as $_middleware) { + $copy = call_user_func($_middleware, $copy); + } + } + + foreach ($this->date_fields as $field) { + $copy[$field] = $this->convertExcelDateValue($item_info[$field]); + } + + foreach ($this->hour_minute_fields as $field) { + $copy[$field] = $this->convertExcelHourMinuteValue($item_info[$field]); + } + + return $this->formatItemInfoBeforeProcess($copy); + } + +} diff --git a/inc/Hura8/System/Controller/aPublicEntityBaseController.php b/inc/Hura8/System/Controller/aPublicEntityBaseController.php new file mode 100644 index 0000000..c305542 --- /dev/null +++ b/inc/Hura8/System/Controller/aPublicEntityBaseController.php @@ -0,0 +1,81 @@ +formatItemInList($item); + }, $this->iEntityModel->getListByIds($list_id, $condition)); + + if($this->iEntityLanguageModel) { + $item_list_language_info = $this->iEntityLanguageModel->getListByIds($list_id); + + $final_list = []; + foreach ($item_list as $item) { + $item_language_info = isset($item_list_language_info[$item['id']]) ? $item_list_language_info[$item['id']] : ["not_translated" => true]; + $final_list[] = array_merge($item, $item_language_info); + } + + return $final_list; + } + + return $item_list; + + } + + + public function getList(array $condition) : array + { + $copy = $condition; + + // public items must have status=1 + $copy['status'] = 1; + + return parent::getList($copy); + } + + + public function getTotal(array $condition) : int + { + $copy = $condition; + + // public items must have status=1 + $copy['status'] = 1; + + return parent::getTotal($copy); + } + + + public function getInfo($id): ?array + { + if(!$id) return null; + + return self::getCache("getInfo-".$id."-".$this->view_language, function () use ($id){ + + $info = $this->iEntityModel->getInfo($id); + + if($this->iEntityLanguageModel && $info) { + $item_language_info = $this->iEntityLanguageModel->getInfo($id); + if($item_language_info) { + return $this->formatItemInfo(array_merge($info, $item_language_info)); + }else{ + $info["not_translated"] = true; + + return $this->formatItemInfo($info); + } + } + + return ($info) ? $this->formatItemInfo($info) : null; + + }); + + } + + +} diff --git a/inc/Hura8/System/Controller/bFileHandle.php b/inc/Hura8/System/Controller/bFileHandle.php new file mode 100644 index 0000000..7de9d70 --- /dev/null +++ b/inc/Hura8/System/Controller/bFileHandle.php @@ -0,0 +1,270 @@ +permit_file_extensions = $permit_file_extensions; + } + + if($target_dir) { + $this->target_dir = PUBLIC_DIR . DIRECTORY_SEPARATOR. $target_dir; + $this->public_dir = "/".$target_dir; + } + + $setup_res = $this->setUp(); + if($setup_res->getStatus() == AppResponse::SUCCESS) { + $this->setup_success = true; + } + } + + + /** + * @description (rename if set) and resize file + * @param string $name + * @param string $new_name + * @param array $resized_sizes [] array('small' => ['width' => 100, 'height' => 100], 'large' => ['width' => 200, 'height' => 200] ) + * @return array|false + */ + public function resizeFile(string $name, string $new_name = '', array $resized_sizes = []) { + + if($new_name) { + $renamed = $this->renameFile($name, $new_name); + if(!$renamed) { + return false; + } + + //$file_name = $renamed['file_name']; + //$public_path = $renamed['public_path']; + $local_path = $renamed['local_path']; + }else{ + //$file_name = $name; + //$public_path = $this->public_dir . "/".$name; + $local_path = $this->target_dir . "/" . $name; + } + + $objHuraImage = new HuraImage(); + + list(, $expected_files, ) = $objHuraImage->resize($local_path, $resized_sizes); + + return $expected_files; + } + + + /** + * @description we can rename the uploaded file to new file name, for example: we want product's image have the format [PRODUCT_ID]-name + * @param string $name + * @param string $new_name + * @return array | false + */ + public function renameFile(string $name, string $new_name) { + if(@rename($this->target_dir . "/" . $name, $this->target_dir . "/" . $new_name)){ + return [ + "file_name" => $new_name, + "public_path" => $this->public_dir . "/".$new_name, + "local_path" => $this->target_dir . "/" . $new_name, + ]; + } + + return false; + } + + + // public utility + public static function getFileExtension($file_name) { + return strtolower(strrchr($file_name,".")); + } + + + /** + * @description run clean up after finish using the FileHandle instance + */ + public function cleanUp() { + if($this->tmp_folder) { + FileSystem::removeDir($this->tmp_folder); + } + } + + protected function processFile( + $original_file_name, + $original_file_tmp_name, // temporary uploaded file as in case of $_FILES[$input_file_name]["tmp_name"] + $fixed_file_name="", + $max_file_size=0 + ) : FileHandleResponse { + + if(!$original_file_name) { + return new FileHandleResponse(AppResponse::ERROR, 'no file', null); + } + + $file_size = filesize($original_file_tmp_name); + if($max_file_size > 0 && $max_file_size < $file_size) { + return new FileHandleResponse(AppResponse::ERROR, 'Size is too large: '.round($file_size/1000).'KB', null); + } + + //validate extension + $file_ext = self::getFileExtension($original_file_name); + + if(!in_array($file_ext, $this->permit_file_extensions)) { + return new FileHandleResponse(AppResponse::ERROR, "Type ".$file_ext." is not allowed!", null); + } + + $file_name = substr($original_file_name, 0, strrpos($original_file_name,".")); + $file_name = preg_replace("/[^a-z0-9_-]/i","", $file_name); + $file_name = substr($file_name, 0, 50); // max- length + $clean_file_name = ($fixed_file_name) ?: $file_name . $file_ext; + + $tmp_file_path = $this->tmp_folder . "/". $clean_file_name; + $return_data = null; + + //debug_var([$original_data, $tmp_file_path]); + if(@rename($original_file_tmp_name, $tmp_file_path)){ + + $is_file_image = (in_array($file_ext, static::$image_extensions )); + + if($is_file_image) { + list($width, $height) = getimagesize($tmp_file_path); + }else{ + $width = 0; + $height = 0; + } + + $detector = new FinfoMimeTypeDetector(); + $mimeType = $detector->detectMimeTypeFromPath($tmp_file_path); + + // if image, we re-create and optimize it + if($is_file_image) { + if(in_array($mimeType, $this->permit_mine_types)) { + $objHuraImage = new HuraImage(); + if($objHuraImage->create($tmp_file_path, $this->target_dir . DIRECTORY_SEPARATOR . $clean_file_name)){ + $return_data = new FileHandleInfo([ + "file_name" => $clean_file_name, + "public_path" => $this->public_dir . "/".$clean_file_name, + "local_path" => $this->target_dir . "/" . $clean_file_name, + "mime_type" => $mimeType, + "file_size" => $file_size, + "file_ext" => $file_ext, + "width" => $width, + "height" => $height, + ]); + } + } + + }elseif(@rename ($tmp_file_path, $this->target_dir . DIRECTORY_SEPARATOR . $clean_file_name )) { + $return_data = new FileHandleInfo([ + "file_name" => $clean_file_name, + "public_path" => $this->public_dir . "/".$clean_file_name, + "local_path" => $this->target_dir . "/" . $clean_file_name, + "mime_type" => $mimeType, + "file_size" => $file_size, + "file_ext" => $file_ext, + "width" => 0, + "height" => 0, + ]); + } + } + + // delete tmp file on server + if(file_exists($original_file_tmp_name)) { + @unlink($original_file_tmp_name); + } + + if($return_data) { + return new FileHandleResponse(AppResponse::SUCCESS, 'Success', $return_data); + } + + return new FileHandleResponse(AppResponse::ERROR, 'Unknown', null); + } + + + protected function setUp(): AppResponse + { + // check target dir + if($this->target_dir && !is_dir($this->target_dir)) { + @mkdir($this->target_dir, 0755, true); + } + + if(!file_exists($this->target_dir)) { + return new AppResponse(AppResponse::ERROR, $this->target_dir.' not exists'); + } + + // create tmp_folder to upload file to + $this->tmp_folder = $this->create_tmp_folder(); + if(!$this->tmp_folder) { + return new AppResponse(AppResponse::ERROR, "Check ".$this->tmp_dir." and make sure it exists and writable"); + } + + return new AppResponse(AppResponse::SUCCESS); + } + + + protected function create_tmp_folder() : ?string { + $tmp_folder = $this->tmp_dir . IDGenerator::createStringId(5); + if(!@mkdir($tmp_folder, 0777, true)) { + return null; + } + + // retest + if(!$tmp_folder || !is_dir($tmp_folder)) { + return null; + } + + return $tmp_folder; + } + + +} diff --git a/inc/Hura8/System/CopyFileFromUrl.php b/inc/Hura8/System/CopyFileFromUrl.php new file mode 100644 index 0000000..a19daaf --- /dev/null +++ b/inc/Hura8/System/CopyFileFromUrl.php @@ -0,0 +1,56 @@ +setup_success) { + return new FileHandleResponse(AppResponse::ERROR, "Fail to setup"); + } + + $original_file_name = substr($file_url, strrpos($file_url, "/")); + $original_file_path = $this->tmp_folder . "/". $original_file_name; + + $url_content = Url::getUrlContent($file_url); + if(!$url_content || !@file_put_contents($original_file_path, $url_content)) { + return new FileHandleResponse(AppResponse::ERROR, "Fail to copy"); + } + + /*if(!@copy($file_url, $original_file_path)) { + return new FileHandleResponse(AppResponse::ERROR, "Fail to copy"); + }*/ + + $result = $this->processFile( + $original_file_name, + $original_file_path, + $fixed_file_name, + $max_file_size + ); + + unset($url_content); + + return $result; + } + + +} diff --git a/inc/Hura8/System/CronProcess.php b/inc/Hura8/System/CronProcess.php new file mode 100644 index 0000000..600c7df --- /dev/null +++ b/inc/Hura8/System/CronProcess.php @@ -0,0 +1,66 @@ +pid = $pid; + $this->objFileSystem = new FileSystem(self::LOG_DIR); + $this->max_allow_run_time = $max_allow_run_time; + } + + public function getPidFile() { + return $this->objFileSystem->getFile($this->pid); + } + + /** + * Check if the process is running. True if: + * - has pid file + * - the file's create-time within the time() and time() - $this->max_allow_run_time + * + * Else: False and remove pid file if exist + * @return bool + */ + public function isRunning() : bool { + + $file_exist = ($this->objFileSystem->getFile($this->pid)); + if(!$file_exist) { + return false; + } + + $last_modified_time = $this->objFileSystem->getFileLastModifiedTime($this->pid); + + if($last_modified_time > 0 && $last_modified_time < time() - $this->max_allow_run_time) { + + echo "Pid File exists but too old. Trying to auto-delete now". PHP_EOL; + + if($this->objFileSystem->delete($this->pid)){ + echo "File is deleted successfully". PHP_EOL; + }else{ + echo "Auto-delete fails. File needs to be removed manually. Path: ". $this->objFileSystem->getFilename($this->pid). PHP_EOL; + } + + return false; + } + + return true; + } + + public function start() { + return $this->objFileSystem->writeFile($this->pid, time()); + } + + public function finish() { + return $this->objFileSystem->delete($this->pid); + } + +} diff --git a/inc/Hura8/System/Email.php b/inc/Hura8/System/Email.php new file mode 100644 index 0000000..b1d3602 --- /dev/null +++ b/inc/Hura8/System/Email.php @@ -0,0 +1,251 @@ + "", + "from_name" => "", + "from_email" => "", + "reply_to" => "", + "subject" => "", + "content" => "", + ]; + + /* @var SettingController $objSettingController */ + protected $objSettingController; + + //construct + public function __construct() { + // default + if(defined("MAIL_METHOD")) $this->config["mail_method"] = MAIL_METHOD; + if(defined("MAIL_NAME")) $this->config["from_name"] = MAIL_NAME; + if(defined("MAIL_ADDRESS")) $this->config["from_email"] = MAIL_ADDRESS; + + if(!$this->config["from_name"]) $this->config["from_name"] = str_replace('www.', '', $_SERVER['HTTP_HOST']); + + $this->objSettingController = new SettingController(); + + $email_settings = $this->objSettingController->getList([ + 'email_from_default', + ]); + + $email_from = (isset($email_settings['email_from_default']) && $email_settings['email_from_default']) ? $email_settings['email_from_default'] : '' ; + if($email_from && self::validate_email($email_from) && $email_from != MAIL_ADDRESS) { + $this->setUp([ + "from_email" => $email_from, + ]); + } + } + + public function setUp(array $config) + { + // over-ride default config + if(sizeof($config)) { + foreach ($config as $key => $value) { + $this->config[$key] = $value; + } + } + } + + public function send(array $to_emails, $subject, $content, array $key_values = []) + { + if(!$this->checkConfig()) return [ + 'status' => 'error', + 'message' => "Config errors: ".\json_encode($this->config), + ]; + + // send email using a template in email/ + $this->config['subject'] = TemplateController::parse(null, $subject, $key_values); + $this->config['content'] = TemplateController::parse(null, $content, $key_values); + + try { + switch ($this->config["mail_method"]){ + case "huraserver"; + $result = $this->sendEmailFromHura($to_emails); + break; + case "queue"; + $result = $this->addQueue($to_emails); + break; + case "adman"; + $result = $this->sendFromAdman($to_emails); + break; + case "smtp"; + $result = $this->sendFromSmtp($to_emails); + break; + default; + $result = null; + } + }catch (\Exception $exception) { + $result = "Error: ".$exception->getMessage(); + } + + if($result) { + $this->log(array_merge($this->config, [ + 'to_emails' => $to_emails, + 'result' => $result, + ])); + } + + return $result; + } + + protected function checkConfig() { + return ( + $this->config["mail_method"] + && $this->config["from_name"] + && $this->config["from_email"] + && self::validate_email($this->config["from_email"]) + ); + } + + protected function addQueue(array $to_emails){ + // todo: + /*foreach($to_email_list as $single_email){ + $this->createQueue(array( + "message_id" => 0, + "from_email" => $from_mail, + "from_name" => $from_name, + "reply_to" => $reply_to_email, + "to_email" => $single_email, + "to_name" => $to_name, + "subject" => $title, + "content" => $content, + "source" => "", + )); + }*/ + return true; + } + + protected function sendFromAdman(array $to_emails){ + if(!defined("ADMAN_API_KEY") || ADMAN_API_KEY == '') return 'ADMAN_API_KEY not set'; + if(!defined("ADMAN_USER_DOMAIN") || ADMAN_USER_DOMAIN == '') return 'ADMAN_USER_DOMAIN not set'; + + $recipients = array_map(function ($email){ + return [ + 'to_email' => $email, + 'to_name' => '', + 'personal_settings' => array(), + ]; + }, $to_emails); + + $payload = array( + 'message' => array( + 'from_email' => $this->config['from_email'], + 'from_name' => $this->config['from_name'], + 'reply_to_email' => $this->config['reply_to'], + 'subject' => $this->config['subject'], + 'settings' => array(), + 'schedule_go_time_utc' => 0, + 'content_html' => $this->config['content'], + 'content_plain' => '', + ), + 'recipients' => $recipients + ); + + return Adman::send(ADMAN_USER_DOMAIN, ADMAN_API_KEY, $payload); + } + + + protected function sendFromSmtp(array $to_emails){ + + if(!SMTP_HOST || !SMTP_USERNAME || !SMTP_PASSWORD) { + return 'Smtp credentials not set'; + } + + // https://swiftmailer.symfony.com/docs/introduction.html + // Create the Transport + $transport = (new Swift_SmtpTransport(SMTP_HOST, SMTP_PORT, SMTP_SECURE)) + ->setUsername(SMTP_USERNAME) + ->setPassword(SMTP_PASSWORD) + ; + $transport->setTimeout(10); + + // Create the Mailer using your created Transport + $mailer = new Swift_Mailer($transport); + + // Create a message + $message = (new Swift_Message()) + ->setSubject($this->config['subject']) + ->setFrom($this->config['from_email'], $this->config['from_name']) + ->setTo($to_emails) + ->setBody($this->config['content'], 'text/html') + ; + + // Send the message + return $mailer->send($message); + } + + + protected function sendEmailFromHura(array $to_emails){ + $rebuild_to_emails = []; + $max_email_allow = 3; + $counter = 0; + foreach ($to_emails as $email) { + if(self::validate_email($email)) { + $counter += 1; + if($counter >= $max_email_allow) break; + $rebuild_to_emails[] = ['email' => $email, 'name' => '']; + } + } + + if(!sizeof($rebuild_to_emails)) { + return [ + 'status' => 'error', + 'message' => 'Error. No valid emails to be sent', + ]; + } + + $data = [ + 'title' => $this->config['subject'], + 'content' => $this->config['content'], + 'to_emails' => $rebuild_to_emails, + 'from_email' => $this->config['from_email'], + 'from_name' => $this->config['from_name'], + 'reply_to_email' => $this->config['reply_to'], + 'reply_to_name' => '', + 'client_id' => CLIENT_ID, + 'domain' => CURRENT_DOMAIN, + ]; + + // protect + $msg_hash = sha1(serialize($data).'asdas32'); + $data['msg_hash'] = $msg_hash; + + $headers = []; + + return curl_post(HURAMAIL_RECEIVE_ENDPOINT, $data, $headers); + } + + protected function createQueue(array $config) { + // todo: + } + + //email log after sending an email and remove from queue + protected function log(array $log_content){ + /*$db = ConnectDB::getInstance(''); + $db->insert( + 'email_log', + [ + 'payload' => \json_encode($log_content), + 'create_time' => CURRENT_TIME, + ] + );*/ + } + + //validate email + public static function validate_email($email){ + return filter_var($email, FILTER_VALIDATE_EMAIL); + } + +} diff --git a/inc/Hura8/System/Export.php b/inc/Hura8/System/Export.php new file mode 100644 index 0000000..dd4d2c4 --- /dev/null +++ b/inc/Hura8/System/Export.php @@ -0,0 +1,47 @@ +setHeaderXLS($file_name); + } + else if($type=='doc') { + $this->setHeaderDoc($file_name); + } + else if($type=='csv') { + $this->setHeaderCSV($file_name); + } + + echo $content; + } + + // method for Excel file + protected function setHeaderXLS($file_name) + { + header("Content-type: application/ms-excel"); + header("Content-Disposition: attachment; filename=$file_name"); + header("Pragma: no-cache"); + header("Expires: 0"); + } + + // method for Doc file + protected function setHeaderDoc($file_name) + { + header("Content-type: application/x-ms-download"); + header("Content-Disposition: attachment; filename=$file_name"); + header('Cache-Control: public'); + } + + // method for CSV file + protected function setHeaderCSV($file_name) + { + header("Content-type: application/csv"); + header("Content-Disposition: inline; filename=$file_name"); + } +} diff --git a/inc/Hura8/System/ExportExcelUseTemplate.php b/inc/Hura8/System/ExportExcelUseTemplate.php new file mode 100644 index 0000000..82efbbf --- /dev/null +++ b/inc/Hura8/System/ExportExcelUseTemplate.php @@ -0,0 +1,158 @@ +_createActiveSheet($tpl_file); + } + } + + + protected function _createActiveSheet($tpl_file) { + $this->spreadsheet = IOFactory::load($tpl_file); + + $this->spreadsheet->setActiveSheetIndex($this->active_sheet_index); + + $this->activeSheet = $this->spreadsheet->getActiveSheet(); + } + + + public function setRowHeight($row_number, $height_pt) { + $this->activeSheet->getRowDimension($row_number)->setRowHeight($height_pt, 'pt'); + } + + + public function removeRow($row_id) { + if(!$this->activeSheet) { + return false; + } + + $this->activeSheet->removeRow($row_id); + + return true; + } + + + /** + * Set a cell value. + * + * @param string $cell Coordinate of the cell, eg: 'A1' + * @param mixed $data Value of the cell + * @param string $pDataType Explicit data type, see DataType::TYPE_* + * + */ + public function writeToCell($cell, $data, $pDataType) { + if(!$this->activeSheet) { + return false; + } + + $this->activeSheet->setCellValueExplicit($cell, $data, $pDataType); + + return true; + } + + + /** + * @param int $start_row + * @param array $row_list array( ['field-1' => value, 'field-2' => value, 'field-3' => value], ['field-1' => value, 'field-2' => value,],) + * @param array $map_column_fields map excel column to row field . Example: array('A' => 'name', 'B' => 'price', ...) + * @param array $number_fields list of data fields that their values will be write to cell as number. Example ['price', 'qty', ] + * @param array $hyperlink_fields list of data fields that their values will be added hyperlink + * @return false|int|mixed|string + */ + public function writeRowList($start_row = 1, array $row_list =[], array $map_column_fields = [], array $number_fields = [], array $hyperlink_fields = []) { + + if(!$this->activeSheet) { + return false; + } + + // add new rows to hold new data + for($i = 0; $i< sizeof($row_list); $i++) { + $this->activeSheet->insertNewRowBefore($start_row + 1); + } + + $last_insert_row_index = 0; + + foreach ($row_list as $index => $item) { + + $last_insert_row_index = $start_row + $index; + + foreach ($map_column_fields as $column => $field) { + + $cell_cor = $column . $last_insert_row_index; + + if(array_key_exists($field, $number_fields)) { + $this->activeSheet->setCellValueExplicit($cell_cor, $item[$field], DataType::TYPE_NUMERIC); + }else{ + $this->activeSheet->setCellValueExplicit($cell_cor, $item[$field], DataType::TYPE_STRING); + } + + // set hyperlink + if(array_key_exists($field, $hyperlink_fields)) { + + // link can be a field of the item or a fixed value + $hyperlink_url = (isset($item[$hyperlink_fields[$field]])) ? $item[$hyperlink_fields[$field]] : $hyperlink_fields[$field]; + + $this->activeSheet->setHyperlink($cell_cor, new Hyperlink($hyperlink_url, 'Mở link để xem tại website')); + } + + // format number + if(array_key_exists($field, $number_fields)) { + + // check format type + if($number_fields[$field]) { + $this->activeSheet->getStyle($cell_cor)->getNumberFormat()->setFormatCode($number_fields[$field]); + } + + }else{ + $this->activeSheet->getStyle($cell_cor)->getAlignment()->setWrapText(true); + } + + } + } + + return $last_insert_row_index; + } + + + public function exportToBrowser($file_name='export') { + if(!$this->spreadsheet) { + die("No file"); + } + + // export + header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + header('Content-Disposition: attachment;filename="'.$file_name.'-'.time().'.xlsx"'); + header('Cache-Control: max-age=0'); + + $writer = new Xlsx($this->spreadsheet); + ob_end_clean(); + $writer->save('php://output'); + + // clean up + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + +} diff --git a/inc/Hura8/System/FileSystem.php b/inc/Hura8/System/FileSystem.php new file mode 100644 index 0000000..38f07b8 --- /dev/null +++ b/inc/Hura8/System/FileSystem.php @@ -0,0 +1,212 @@ +umask = $umask; + + if (! $this->createPathIfNeeded($directory)) { + throw new \Exception(sprintf( + 'The directory "%s" does not exist and could not be created.', + $directory + )); + } + + if (! is_writable($directory)) { + throw new \Exception(sprintf( + 'The directory "%s" is not writable.', + $directory + )); + } + + // YES, this needs to be *after* createPathIfNeeded() + $this->directory = realpath($directory); + + //$this->directoryStringLength = strlen($this->directory); + //$this->isRunningOnWindows = defined('PHP_WINDOWS_VERSION_BUILD'); + } + + + /** + * @description extract a zip file after it has been uploaded to the server + * @param string $filename name of the zip file (abc.zip) + * @param string $uploaded_zip_path path where it's uploaded to i.e /usr/.../tmp_upload/ + * @param string $extract_target_path path where the zip files will be extracted to , if not provided the $uploaded_zip_path will be used + * @return array [extracted_folder, test_folder_exist] + */ + public static function unzip(string $filename, string $uploaded_zip_path, string $extract_target_path = '') { + $extract_path = $extract_target_path ?: $uploaded_zip_path; + + $zip = new \ZipArchive(); + $x = $zip->open($uploaded_zip_path . DIRECTORY_SEPARATOR . $filename); + if ($x === true) { + $zip->extractTo($extract_path); // change this to the correct site path + $zip->close(); + } + @unlink($uploaded_zip_path . DIRECTORY_SEPARATOR . $filename); + + $expected_result = $extract_path . DIRECTORY_SEPARATOR . str_replace(".zip", "", $filename); + + return array($expected_result, file_exists($expected_result)); + } + + + /** + * @description scan a folder recursively and return all files with sub-path + * @param string $folder full path /usr/local/.../ + * @param array $file_list + */ + public static function scanDirRecursive(string $folder, array &$file_list = []) { + $dir = opendir($folder); + while(( $file = readdir($dir)) ) { + if (( $file != '.' ) && ( $file != '..' )) { + if ( is_dir($folder. '/' . $file) ) { + FileSystem::scanDirRecursive($folder. '/' . $file, $file_list); + } + else { + //$file_list[] = str_replace(PUBLIC_DIR . "/", "", $folder .'/'. $file); + $file_list[] = $folder .'/'. $file; + } + } + } + closedir($dir); + } + + //remove directory recursively + public static function removeDir($dir) { + if (is_dir($dir)) { + $objects = scandir($dir); + foreach ($objects as $object) { + if ($object != "." && $object != "..") { + if (is_dir($dir."/".$object)) { + self::removeDir($dir."/".$object); + } + else { + @unlink($dir."/".$object); + } + } + } + @rmdir($dir); + } + } + + public function getFile(string $filename) + { + $full_filepath = $this->getFilename($filename); + if(file_exists($full_filepath)) { + return file_get_contents($full_filepath); + } + + return false; + } + + public function getFileLastModifiedTime(string $filename) + { + $full_filepath = $this->getFilename($filename); + if(file_exists($full_filepath)) { + return filemtime($full_filepath); + } + + return 0; + } + + /** + * Writes a string content to file in an atomic way. + * + * @param string $filename Path to the file where to write the data. + * @param string $content The content to write + * + * @return bool TRUE on success, FALSE if path cannot be created, if path is not writable or an any other error. + */ + public function writeFile(string $filename, string $content) + { + $full_filepath = $this->getFilename($filename); + $filepath = pathinfo($full_filepath, PATHINFO_DIRNAME); + + if (! $this->createPathIfNeeded($filepath)) { + return false; + } + + if (! is_writable($filepath)) { + return false; + } + + $tmpFile = tempnam($filepath, 'swap'); + @chmod($tmpFile, 0666 & (~$this->umask)); + + if (file_put_contents($tmpFile, $content) !== false) { + + @chmod($tmpFile, 0666 & (~$this->umask)); + if (@rename($tmpFile, $full_filepath)) { + return true; + } + + @unlink($tmpFile); + } + + return false; + } + + public function delete($filename) + { + $full_filepath = $this->getFilename($filename); + + return @unlink($full_filepath) || ! file_exists($full_filepath); + } + + /** + * @param string $filename + * + * @return string + */ + public function getFilename($filename) + { + return $this->directory + . DIRECTORY_SEPARATOR + . $filename ; + } + + /** + * Create path if needed. + * + * @return bool TRUE on success or if path already exists, FALSE if path cannot be created. + */ + private function createPathIfNeeded(string $path) + { + if (! is_dir($path)) { + if (@mkdir($path, 0755 & (~$this->umask), true) === false && ! is_dir($path)) { + return false; + } + } + + return true; + } + +} diff --git a/inc/Hura8/System/FileUpload.php b/inc/Hura8/System/FileUpload.php new file mode 100644 index 0000000..a25bbde --- /dev/null +++ b/inc/Hura8/System/FileUpload.php @@ -0,0 +1,110 @@ +handleUpload("file"); + + if($upload_result->getStatus() == 'ok') { + $file_info = $upload_result->getData(); + + $file_name = $file_info->file_name; + $file_ext = $file_info->file_ext; + $public_path = $file_info->public_path; + + $renamed = $objFileUpload->renameUploadedFile($file_name, $item_id . $file_ext); + + if($renamed) { + $new_file_name = $renamed['file_name']; + $new_public_path = $renamed['public_path']; + } + + } + * + */ +class FileUpload extends bFileHandle +{ + + /** + * @description version of handleUpload to support multiple files using array input + * example: instead of + * + * @param string $input_file_name name of => use: multiple_file + * @param int $max_file_size max file size in bytes accepts, default = 1MB + * @return FileHandleResponse[] + */ + public function handleMultipleUpload(string $input_file_name, int $max_file_size = 1000000) : array + { + $upload_result = []; + for($i = 0; $i < sizeof($_FILES[$input_file_name]['name']); $i++){ + $original_uploaded_file_name = $_FILES[$input_file_name]["name"][$i]; + $original_uploaded_data = $_FILES[$input_file_name]["tmp_name"][$i]; + + $upload_result[] = $this->processFile( + $original_uploaded_file_name, + $original_uploaded_data, + '', + $max_file_size + ); + } + + // clean up + $this->cleanUp(); + + return $upload_result; + } + + + /** + * @param string $input_file_name name of (which is upload_file) + * @param int $max_file_size max file size in bytes accepts, default = 1MB + * @param string $fixed_file_name set uploaded name to this fixed name (ie. [item_id].jpg) so old file will be replaced + * @return FileHandleResponse + */ + public function handleUpload(string $input_file_name, string $fixed_file_name='', int $max_file_size = 1000000) : FileHandleResponse + { + if(!$this->setup_success) { + return new FileHandleResponse(AppResponse::ERROR, "Fail to setup"); + } + + if(!$input_file_name || !isset($_FILES[$input_file_name])) { + return new FileHandleResponse('error', 'no file', null); + } + + $original_uploaded_file_name = $_FILES[$input_file_name]["name"] ?? ""; + $original_uploaded_data = $_FILES[$input_file_name]["tmp_name"]; + + $upload_result = $this->processFile( + $original_uploaded_file_name, + $original_uploaded_data, + $fixed_file_name, + $max_file_size + ); + + // clean up + $this->cleanUp(); + + return $upload_result; + } + + +} diff --git a/inc/Hura8/System/Firewall.php b/inc/Hura8/System/Firewall.php new file mode 100644 index 0000000..b148831 --- /dev/null +++ b/inc/Hura8/System/Firewall.php @@ -0,0 +1,190 @@ +log_dir = ROOT_DIR . '/cache/firewall/log/'; + $this->ban_dir = ROOT_DIR . '/cache/firewall/ban/'; + if(!file_exists($this->log_dir)) { + mkdir($this->log_dir, 0755, true); + } + if(!file_exists($this->ban_dir)) { + mkdir($this->ban_dir, 0755, true); + }*/ + $this->user_ip = getIpAddress(); + + if(file_exists(self::WHITE_LIST_IP)) { + $this->white_list = array_filter(explode("\n", file_get_contents(self::WHITE_LIST_IP))); + } + + } + + + public static function getInstance() { + if(!static::$instance) { + static::$instance = new self(); + } + + return static::$instance; + } + + // check if current user agent is an crawler + public function isCrawler() : bool { + $objCrawlerDetect = new CrawlerDetect(); + //echo $objCrawlerDetect->getUserAgent(); + // https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers + return $objCrawlerDetect->isCrawler(); + } + + public function monitor() { + // todo: + return false; + + $total_concurrent_visit = $this->logIp($this->user_ip); + if($total_concurrent_visit > self::MAX_CONCURRENT_VISIT_ALLOW) { + // ban if not in whitelist + if(!$this->isWhiteListed()) { + $this->banTemporary($this->user_ip, self::TEMPORARY_BAN_DURATION); + } + + die("System overload"); + } + + return true; + } + + public function stopIfBanned() { + if($this->isIPBanned()) { + die("Perhaps, this website is not for you."); + } + + return false; + } + + protected function isWhiteListed() { + $filter = new IPFilter($this->white_list); + return $filter->check($this->user_ip); + } + + protected function isIPBanned() { + + // check whitelist + if($this->isWhiteListed()) { + return false; + } + + // check temporary ban + /*$file_id = $this->ban_dir . $this->user_ip.'.data'; + if(file_exists($file_id)) { + $lift_time = file_get_contents($file_id); + // lift_time is not over yet + if($lift_time > time()) { + return true; + } + }*/ + + // permanent ban + $config_file = static::BAN_IP_LIST; + if(@file_exists( $config_file )) { + $list_ips = @file_get_contents( $config_file ); + $list_ip_arr = array_filter(explode("\n", $list_ips)); + $filter = new IPFilter($list_ip_arr); + + return $filter->check($this->user_ip); + } + + return false; + } + + // ban an IP temporarily + protected function banTemporary($ip, $time) { + $file_id = $this->ban_dir . $ip.'.data'; + @file_put_contents($file_id, time() + $time); + } + + + + // only allow 1 of these formats: + // - Full IP: 127.0.0.1 + // - Wildcard: 172.0.0.* + // - Mask: 125.0.0.1/24 + // - Range: 125.0.0.1-125.0.0.9 + public static function validateIP($ip) { + // - Full IP: 127.0.0.1 + if (filter_var($ip, FILTER_VALIDATE_IP)) { + return true; + } + + // - Wildcard: 172.0.0.* + if (strpos($ip, "*") !== false && preg_match("/^([0-9]+)\.([0-9]+)\.(([0-9]+)|\*)\.(([0-9]+)|\*)$/i", $ip)) { + return true; + } + + // - Mask: 125.0.0.1/24 + if (strpos($ip, "/") !== false && preg_match("/^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)\/([0-9]+)$/i", $ip)) { + return true; + } + + // - Range: 125.0.0.1-125.0.0.9 + if (strpos($ip, "-") !== false ) { + list($begin, $end) = explode('-', $ip); + return filter_var($begin, FILTER_VALIDATE_IP) && filter_var($end, FILTER_VALIDATE_IP); + } + + return false; + } + + + private function logIp($ip) { + $file_id = $this->log_dir . $ip.'.data'; + $time_track = CURRENT_TIME - CURRENT_TIME % 5; // track within 5 seconds, + $data = []; + if(file_exists($file_id)) { + $data = \json_decode(file_get_contents($file_id), true); + } + + if(isset($data[$time_track])) { + $data[$time_track] = $data[$time_track] + 1; + }else{ + $data[$time_track] = 1; + } + + // only log for current time to prevent large $data's size + // our purpose is only to detect if this IP is brutally forceing the system (too many visits per time) + + @file_put_contents($file_id, \json_encode($data)); + + return $data[$time_track]; + } + +} diff --git a/inc/Hura8/System/HtmlParser.php b/inc/Hura8/System/HtmlParser.php new file mode 100644 index 0000000..61175a0 --- /dev/null +++ b/inc/Hura8/System/HtmlParser.php @@ -0,0 +1,245 @@ +source_html = str_replace(array("\n", "\r", "\t")," ", $source_html); + } + + //@$boundary_pattern: pattern to find a smaller boundary-block within source_html so that our content does not wander around + /** + * @param $pattern_arr_or_str array|string ['pattern1', 'pattern2'] or 'pattern' + * @param bool $match_once + * @param bool $find_image + * @param string $boundary_pattern + * @return array|bool + */ + public function extract($pattern_arr_or_str, $match_once = true, $find_image = false, $boundary_pattern = "") { + if(is_array($pattern_arr_or_str)) { + foreach ($pattern_arr_or_str as $pattern){ + $result = $this->extractSinglePattern($pattern, $match_once, $find_image, $boundary_pattern); + if(is_array($result) && isset($result['result'])) { + return $result; + } + } + + return false; + } + + // default is string + return $this->extractSinglePattern($pattern_arr_or_str, $match_once, $find_image, $boundary_pattern); + } + + /** + * @param $pattern string + * @param bool $match_once + * @param bool $find_image + * @param string $boundary_pattern + * @return array|bool + */ + protected function extractSinglePattern($pattern, $match_once = true, $find_image = false, $boundary_pattern = "") { + + $elements = $this->getCodeElement($pattern); + if(!$elements["code"]) return false; + + $match = array(); + $source_html = $this->source_html; + //found boundary if pattern exist + if($boundary_pattern && preg_match("@".$boundary_pattern."@i", $source_html, $match)) { + $source_html = $match[1]; + } + + if($match_once) { + if(preg_match("@".$elements["code"]."@i", $source_html, $match)){ + //echo $match[1]; + return array( + "result" => $this->cleanHtmlBlock($match[1], $elements), + "images" => ($find_image) ? $this->extractImages($match[1]) : null + ); + } + } else { + $results = array(); + if(preg_match_all("@".$elements["code"]."@", $source_html, $match)){ + foreach ($match[1] as $html_block ) { + $results[] = array( + "result" => $this->cleanHtmlBlock($html_block, $elements), + "images" => ($find_image) ? $this->extractImages($html_block) : null + ); + } + } + + return $results; + } + + return false; + } + + private function cleanHtmlBlock($html_block, $elements) { + + if($elements["removed"]){ + $arrayRemover = array_filter(explode(";",$elements["removed"])); + foreach($arrayRemover as $char_removed){ + $char_removed = stripslashes(trim($char_removed)); + $html_block = str_replace($char_removed.";","", $html_block); //sometimes   + $html_block = str_replace($char_removed,"", $html_block); + $html_block = preg_replace("{".addslashes($char_removed)."}","", $html_block); + } + } + + if($elements["sepa"]){ + $html_block = strrchr($html_block, $elements["sepa"]); + $html_block = str_replace($elements["sepa"],"", $html_block); + } + + if($elements["extra_url"] && $html_block) $html_block = $elements["extra_url"] . trim($html_block); + + //thay the cum tu source bang cum tu tuy chon + if($elements["invalid"]){ + $arrayReplace = array_filter(explode(";", $elements["invalid"])); + foreach($arrayReplace as $replace_group){ + $replace_group = stripslashes($replace_group); + //echo $replace_group; + $replace_group_a = explode("#",$replace_group); + $html_block = str_replace(trim($replace_group_a[0]),trim($replace_group_a[1]), $html_block); + $html_block = preg_replace("{".addslashes(trim($replace_group_a[0]))."}",trim($replace_group_a[1]), $html_block); + } + } + + return trim($html_block); + } + + + private function extractImages($source_html){ + $img_match = array(); + if(preg_match_all("/img(.*?)?src\s*=\s*\\\\?[\'\"]?([+:%\/\?~=&;\(\),|!._a-zA-Z0-9-]*)[\'\"]?/i", $source_html, $img_match)){ + return array_unique($img_match[2]); + } + return array(); + } + + //$code_line = + // singlePro | chandau(.*?)chancuoi | remove_word'];word2 | sepa | prepend_after | invalid_list + // pagePro | chandau(.*?)chancuoi | remove_word'];word2 | sepa | prepend_after | invalid_list + /** + * @param $code_line string + * @return array|false + */ + private function getCodeElement($code_line){ + if(!$code_line) return false; + + $element = explode("|", trim($code_line)); + $result = array(); + if(array_key_exists(0, $element)){ + $result["type"] = trim($element[0]); //pagePro, singlePro + }else $result["type"] = ""; + + if(array_key_exists(1, $element)){ + $result["code"] = trim($element[1]); + }else $result["code"] = ""; + + if(array_key_exists(2,$element)){ + $result["removed"] = trim($element[2]); + }else $result["removed"] = ""; + + if(array_key_exists(3,$element)){ + $result["sepa"] = trim($element[3]); + }else $result["sepa"] = ""; + + if(array_key_exists(4,$element)){ + $result["extra_url"] = trim($element[4]); + }else $result["extra_url"] = ""; + + if(array_key_exists(5,$element)){ + $result["invalid"] = trim($element[5]); //for images + }else $result["invalid"] = ""; + + return $result; + } + + + private function buildFullUrl($url, $base_url){ + if(!$base_url) return $url; + if(strlen($url) < 2) return ""; + + if(preg_match("/(http|www.|javascript|mailto|ymsgr)/i",$url)){ + return $url; + }else{ + return $this->convert_to_absolute( $base_url, $url ); + } + } + + private function convert_to_absolute($absolute, $relative) { + $p = parse_url($relative); + $first_letter = $relative[0]; + $last_letter = substr($relative, strlen($relative) -1, 1); + + if(array_key_exists("scheme",$p) || strpos($relative,"www.") !== false || substr($relative, 0, 2) == '//') return $relative; //it's absolute + + if(in_array($first_letter,array('?',';'))) return str_replace(strrchr($absolute,$first_letter),"",$absolute) . $relative; + + if($first_letter == "#") return $absolute;//already crawled this page + + extract(parse_url($absolute)); + + $path = (isset($path)) ? $path : ""; + $path = (strrchr($absolute,"/")!="/") ? ((dirname($path) != "\\") ? dirname($path) : "") : $path; + + if($first_letter == '/') { + $cparts = array_filter(explode("/", $relative)); + } + else { + + $aparts = array_filter(explode("/", $path)); + //print_r($aparts); + $rparts = array_filter(explode("/", $relative)); + //print_r($rparts); + $cparts = array_merge($aparts, $rparts); + //print_r($cparts); + if(!preg_match("/[a-z0-9]/i",$first_letter)){ + foreach($cparts as $i => $part) { + if($part == '.') { + $cparts[$i] = null; + } + if($part == '..') { + $cparts[$i] = ''; + if(array_key_exists($i - 1,$cparts)){ + if($cparts[$i - 1] != null) $cparts[$i - 1] = null; + else if(array_key_exists($i - 3,$cparts)) $cparts[$i - 3] = null; // in case ../../ + } + } + + } + } + $cparts = array_filter($cparts); + } + + $path = implode("/", $cparts); + if($last_letter == '/') $path .= "/"; + + + $url = ""; + if($scheme) { + $url = "$scheme://"; + } + + if($host) { + $url .= "$host/"; + } + $url .= $path; + return $url; + } + +} diff --git a/inc/Hura8/System/HuraImage.php b/inc/Hura8/System/HuraImage.php new file mode 100644 index 0000000..55bc3b3 --- /dev/null +++ b/inc/Hura8/System/HuraImage.php @@ -0,0 +1,180 @@ +objInterventionImage = new InterventionImage(); + } + + + /** + * optimize file size of an image and resize its width to the max_width it + * + * @param string $file_dir __DIR__."/media/product/' + * @param string $file_name file.jpg + * @param int $max_width + * @return bool + */ + public function optimizeFile(string $file_dir, string $file_name, int $max_width = 1200): bool + { + $file_path = $file_dir . DIRECTORY_SEPARATOR . $file_name; + $random_str = IDGenerator::createStringId(10); + $clone_file = $file_dir . DIRECTORY_SEPARATOR . $random_str. '-' . $file_name; + + if(!copy($file_path, $clone_file)) { + return false; + } + + $img = $this->objInterventionImage->make($clone_file); + + list ( $img_width ) = getimagesize($clone_file); + if($img_width > $max_width) { + $img->resize($max_width, null, function ($constraint) { + $constraint->aspectRatio(); + })->save($file_path, 90); + }else{ + $img->save($file_path, 90); + } + + // then delete the clone + unlink($clone_file); + + return file_exists($file_path); + } + + + /** + * create image to local system + * copy from https://image.intervention.io/v2/api/make + * @param mixed $source + * Source to create an image from. The method responds to the following input types: + string - Path of the image in filesystem. + string - URL of an image (allow_url_fopen must be enabled). + string - Binary image data. + string - Data-URL encoded image data. + string - Base64 encoded image data. + resource - PHP resource of type gd. (when using GD driver) + object - Imagick instance (when using Imagick driver) + object - Intervention\Image\Image instance + object - SplFileInfo instance (To handle Laravel file uploads via Symfony\Component\HttpFoundation\File\UploadedFile) + * + * @param string $saved_file_path __DIR__."/media/product/file.jpg' + * @return bool + */ + public function create($source, $saved_file_path) { + + try { + $this->objInterventionImage->make($source)->save($saved_file_path, 90); + }catch (\Exception $e) { + // NotReadableException: Unsupported image type. GD/PHP installation does not support WebP format + @copy($source, $saved_file_path); + } + + return file_exists($saved_file_path); + } + + + /** + * https://image.intervention.io/v2/api/encode + * @param $local_file_path + * @param string $format jpg | png | gif | webp | data-url + * @return bool + */ + public function convertFileFormat($local_file_path, $format ) { + + // if same format as local file, stop + $file_ext = strtolower(substr(strrchr($local_file_path, '.'), 1)); + if($file_ext == $format) { + return true; + } + + $saved_file_path = substr($local_file_path, 0, strrpos($local_file_path,".") ) . ".". $format; + + $this->objInterventionImage->make($local_file_path) + ->encode($format, 100) + ->save($saved_file_path, 90); + + return file_exists($saved_file_path); + } + + + /** + * guide: https://image.intervention.io/v2/api/resize + * @param $local_file_path + * @param array $size_key_dimension array('small' => ['width' => 100, 'height' => 100], 'large' => ['width' => 200, 'height' => 200] ) + * @param string $resized_file_directory + * @return array + */ + public function resize($local_file_path, array $size_key_dimension, $resized_file_directory = '') { + + $stored_directory = ($resized_file_directory) ?: substr($local_file_path, 0, strrpos($local_file_path,"/") ) ; + //echo "

stored_directory: ".$stored_directory; + + $file_name = substr(strrchr($local_file_path,"/"), 1); + //echo "

file_name: ".$file_name; + + $expected_files = []; + foreach ($size_key_dimension as $key => $dimension) { + + $resized_file_name = $key. IMAGE_FILE_SEPARATOR . $file_name; + $expected_files[] = $resized_file_name; + + $saved_file_path = $stored_directory . "/".$resized_file_name; + $new_width = $dimension['width'] ?? null; + $new_height = $dimension['height'] ?? null; + + $img = $this->objInterventionImage->make($local_file_path); + + if(!$new_width || !$new_height) { + $img->resize($new_width, $new_height, function ($constraint) { + $constraint->aspectRatio(); + })->save($saved_file_path, 90); + }else{ + $img->resize($new_width, $new_height)->save($saved_file_path, 90); + } + + $img->destroy(); + } + + //echo "

Expected files: ".\json_encode($expected_files); + $exist_all = true; + foreach ($expected_files as $_file) { + if(!file_exists($stored_directory . "/". $_file)) { + $exist_all = false; + break; + } + } + + return array($exist_all, $expected_files, $stored_directory); + } + + + /** + * @param $original_file_path + * @param $img_watermark_path + * @param string $position + */ + public function watermark($original_file_path, $img_watermark_path, $position='bottom-right') { + $img = $this->objInterventionImage->make($original_file_path); + $watermark = $this->objInterventionImage->make($img_watermark_path); + $img->insert($watermark, $position, 10, 10); + //$img->bac ($destinationPath.'/'.$fileName); + $img->save($original_file_path, 100); + } + +} diff --git a/inc/Hura8/System/IDGenerator.php b/inc/Hura8/System/IDGenerator.php new file mode 100644 index 0000000..eadaf26 --- /dev/null +++ b/inc/Hura8/System/IDGenerator.php @@ -0,0 +1,56 @@ + final id = P000020 + * @return string + */ + public static function createItemId(string $prefix, int $increment_id, $length = 6): string + { + $num_zeros = $length - strlen($increment_id); + if($num_zeros > 0) { + return join("", [$prefix, str_repeat("0", $num_zeros), $increment_id]); + } + + return join("", [$prefix, $increment_id]); + } + + + private static $nanoClientCache; + + + /** + * @description create string id using Nano ID + * Collision Calculator: https://zelark.github.io/nano-id-cc/ + * @param int $length + * @param bool $lower_case + * @return string + */ + public static function createStringId(int $length = 15, bool $lower_case = false) : string { + + if($lower_case) { + $alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'; + } else { + $alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + } + + if(isset(static::$nanoClientCache)) { + return static::$nanoClientCache->formattedId($alphabet, $length); + } + + // create one + $client = new Client(); + static::$nanoClientCache = $client; + + return $client->formattedId($alphabet, $length); + } + +} diff --git a/inc/Hura8/System/IPFilter.php b/inc/Hura8/System/IPFilter.php new file mode 100644 index 0000000..bd7174d --- /dev/null +++ b/inc/Hura8/System/IPFilter.php @@ -0,0 +1,129 @@ + check('126.1.0.2');*/ + +class IPFilter +{ + private static $_IP_TYPE_SINGLE = 'single'; + private static $_IP_TYPE_WILDCARD = 'wildcard'; + private static $_IP_TYPE_MASK = 'mask'; + private static $_IP_TYPE_SECTION = 'section'; + private $_allowed_ips = array(); + + public function __construct($allowed_ips) + { + $this -> _allowed_ips = $allowed_ips; + } + + + public function check($ip, $allowed_ips = null) + { + $allowed_ips = $allowed_ips ? $allowed_ips : $this->_allowed_ips; + + foreach($allowed_ips as $allowed_ip) + { + $type = $this->_judge_ip_type($allowed_ip); + $sub_rst = call_user_func(array($this,'_sub_checker_' . $type), $allowed_ip, $ip); + + if ($sub_rst) + { + return true; + } + } + + return false; + } + + + private function _judge_ip_type($ip) + { + if (strpos($ip, '*')) + { + return self::$_IP_TYPE_WILDCARD; + } + + if (strpos($ip, '/')) + { + return self::$_IP_TYPE_MASK; + } + + if (strpos($ip, '-')) + { + return self::$_IP_TYPE_SECTION; + } + + return self::$_IP_TYPE_SINGLE; + + /*if (ip2long($ip)) + { + return self::$_IP_TYPE_SINGLE; + } + + return false;*/ + } + + // can use for IPV4&6 + private function _sub_checker_single($allowed_ip, $ip) + { + return strval($allowed_ip) == strval($ip); + //return (ip2long($allowed_ip) == ip2long($ip)); + } + + // currently only IPV4: 172.0.0.* + private function _sub_checker_wildcard($allowed_ip, $ip) + { + $allowed_ip_arr = explode('.', $allowed_ip); + $ip_arr = explode('.', $ip); + for($i = 0;$i < count($allowed_ip_arr);$i++) + { + if ($allowed_ip_arr[$i] == '*') + { + return true; + } + else + { + if (false == ($allowed_ip_arr[$i] == $ip_arr[$i])) + { + return false; + } + } + } + + return true; + } + + // currently only IPV4: 125.0.0.1/24 + private function _sub_checker_mask($banned_range, $ip) + { + // $allowed_ip is in IP/CIDR format eg 127.0.0.1/24 + list( $range, $netmask ) = explode( '/', $banned_range, 2 ); + $range_decimal = ip2long( $range ); + $ip_decimal = ip2long( $ip ); + $wildcard_decimal = pow( 2, ( 32 - $netmask ) ) - 1; + $netmask_decimal = ~ $wildcard_decimal; + return !( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) ); + } + + // currently only IPV4: 125.0.0.1-125.0.0.9 + private function _sub_checker_section($allowed_ip, $ip) + { + list($begin, $end) = explode('-', $allowed_ip); + $begin = ip2long(trim($begin)); + $end = ip2long(trim($end)); + $ip = ip2long($ip); + + return ($ip >= $begin && $ip <= $end); + } +} diff --git a/inc/Hura8/System/Language.php b/inc/Hura8/System/Language.php new file mode 100644 index 0000000..fe4f164 --- /dev/null +++ b/inc/Hura8/System/Language.php @@ -0,0 +1,94 @@ + nguyeexn minh hieesu + public static function convertText($vietnamese_txt){ + + $vietnamese_char = array( + "đ", + "ó","ỏ","ò","ọ","õ","ô","ỗ","ổ","ồ","ố","ộ","ơ","ỡ","ớ","ờ","ở","ợ", + "ì","í","ỉ","ì","ĩ","ị", + "ê","ệ","ế","ể","ễ","ề","é","ẹ","ẽ","è","ẻ", + "ả","á","ạ","ã","à","â","ẩ","ấ","ầ","ậ","ẫ","ă","ẳ","ắ","ằ","ặ","ẵ", + "ũ","ụ","ú","ủ","ù","ư","ữ","ự","ứ","ử","ừ", + "ỹ","ỵ","ý","ỷ","ỳ", + ); + + $equivalent_char = array( + "dd", + "os","or","of","oj","ox","oo","oox","oor","oof","oos","ooj","ow","owx","ows","owf","owr","owj", + "if","is","ir","if","ix","ij", + "ee","eej","ees","eef","eex","eer","es","ej","ex","ef","or", + "ar","as","aj","ax","af","aa","aar","aas","aaf","aaj","aax","aw","awr","aws","awf","awj","aax", + "ux","uj","us","ur","uf","uw","uwx","uwj","uws","uwr","uwf", + "yx","yj","ys","yr","yf", + ); + + return str_replace($vietnamese_char, $equivalent_char, static::convert_lower($vietnamese_txt)); + + } +} diff --git a/inc/Hura8/System/Model/AuthModel.php b/inc/Hura8/System/Model/AuthModel.php new file mode 100644 index 0000000..8c38af1 --- /dev/null +++ b/inc/Hura8/System/Model/AuthModel.php @@ -0,0 +1,297 @@ +tb_login = $tb_login; + $this->tb_access_code = $tb_access_code; + $this->db = get_db('', ENABLE_DB_DEBUG); + } + + + private const ACCESS_CODE_LENGTH = 30; + const ONE_TIME_KEY_LENGTH = 15; + + + public function checkOneTimeKey($auth_key) { + $db_response = $this->db->select( + $this->tb_onetime_key, + ['user_id', 'user_name', 'client_id', 'create_time'], + [ + 'auth_key' => ["=", $auth_key], + ], + '', + 1 + ); + + if($db_response->getCode()) { + return null; + } + + $info = $db_response->getData(); + + if($info) { + + // used ONCE and delete the key + $this->db->runQuery("DELETE FROM `".$this->tb_onetime_key."` WHERE `user_id` = ? ", ['s'], [$info['user_id']]); + + return $info; + } + + return false; + } + + + // auth key allows users to export data (i.e. export to excel) for offline use + public function createNewOneTimeKey($user_id) { + + // and make a new one + $auth_key = IDGenerator::createStringId(self::ONE_TIME_KEY_LENGTH); + + $this->db->insert($this->tb_onetime_key, [ + 'user_id' => $user_id, + 'auth_key' => $auth_key, + 'create_time' => CURRENT_TIME, + ]); + + return $auth_key; + } + + + // for all subsequent requests, API need to provide access-code in the request's header + // the server will verify the code + public function checkAccessCode($access_code) { + return $this->db->select( + $this->tb_access_code, + ['user_id', 'create_time'], + [ + 'access_code' => ["=", $access_code], + ], + '', + 1 + ); + } + + + public function deleteAllAccessCode($user_id) { + $this->db->runQuery("DELETE FROM `".$this->tb_access_code."` WHERE `user_id` = ? ", ['s'], [$user_id]); + } + + + protected function createNewAccessCode($user_id) { + + // when use login here, delete all other access code + $this->deleteAllAccessCode($user_id); + + // and make a new one + $access_code = IDGenerator::createStringId(self::ACCESS_CODE_LENGTH); + $user_device = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'; + + $db_response = $this->db->insert($this->tb_access_code, [ + 'user_id' => $user_id, + 'access_code' => $access_code, + 'user_device' => substr($user_device, 0, 150), + 'create_time' => CURRENT_TIME, + ]); + + return $access_code; + } + + /** + * allow to login via mobile + * - step 1: sms an OTP code to mobile number + * - step 2: verify OTP code (mobile number and otp code sent along in the form) + */ + + public function checkLoginViaMobile($mobile, $otp) { + // todo + } + + /** + * @description An OTP code is sent to user's email. This method helps user need not remember the password + * @param $email string + * @param $otp string + */ + + public function checkLoginByOTP($user_id, $otp) { + $info = $this->db->select( + $this->tb_login, + [], + [ + 'user_id' => ["=", $user_id], + 'login_otp' => ["=", $otp], + ], + '', + 1 + ); + + if($info) { + // return to browser + return array( + 'user_id' => $user_id, + 'access_code' => $this->createNewAccessCode($user_id), + ); + } + + return false; + } + + + public function createLoginOTP($user_id) { + // check email exist + $info = $this->db->select( + $this->tb_login, + [], + [ + 'user_id' => ["=", $user_id], + ], + '', + 1 + ); + + if($info) { + $otp = IDGenerator::createStringId(6); + $this->db->update( + $this->tb_login, + [ + 'login_otp' => $otp + ], + [ + 'user_id' => $info['user_id'], + ] + ); + + return $otp; + } + + return false; + } + + + public function createOrUpdatePassword($user_id, $new_password) { + $query = $this->db->runQuery( + "SELECT `user_id` FROM `".$this->tb_login."` WHERE `user_id` = ? LIMIT 1 ", + ['d'], [ $user_id ] + ); + + if($this->db->fetchAssoc($query)) { + return $this->updatePassword($user_id, $new_password); + } + + return $this->createPassword($user_id, $new_password); + } + + + protected function createPassword($user_id, $new_password) { + return $this->db->insert( + $this->tb_login, + [ + 'user_id' => $user_id, + 'password_hash' => $this->hashPassword($new_password), + 'create_time' => CURRENT_TIME, + 'create_by' => '', + ] + ); + } + + + protected function updatePassword($user_id, $new_password) { + return $this->db->update( + $this->tb_login, + [ + 'password_hash' => $this->hashPassword($new_password), + 'last_update' => CURRENT_TIME, + 'last_update_by' => ADMIN_NAME, + ], + [ + 'user_id' => $user_id, + ] + ); + } + + + /** + * @param $user_id int + * @param $password string + * @return array|false + */ + public function checkLogin($user_id, $password) { + + $info = $this->db->select( + $this->tb_login, + [ 'password_hash'], + [ + 'user_id' => ["=", $user_id], + ], + '', + 1 + ); + + //test password + if($info && $this->verifyHash($password, $info['password_hash'])) { + + $this->updateUserLogin($user_id); + + // return to browser + return array( + 'user_id' => $user_id, + 'access_code' => $this->createNewAccessCode($user_id), + ); + } + + return false; + } + + + private function updateUserLogin($user_id){ + return $this->db->update( + $this->tb_login, + [ + 'last_login_time' => CURRENT_TIME, + 'last_login_ip' => USER_IP, + 'last_login_device' => '', + 'last_login_session_id' => \Hura8\System\Security\Session::id() ?: '', + 'last_login_browser' => USER_AGENT, + ], + [ + 'user_id' => $user_id, + ] + ); + } + + /** + * @param $str string + * @return string + */ + private function hashPassword($str) { + return password_hash($str, PASSWORD_BCRYPT, array('cost' => 12 )); + } + + /** + * 15-04-2016 verify string with given hash + * @param $str_to_verify string + * @param $hash string + * @return boolean + */ + private function verifyHash($str_to_verify, $hash) { + return (password_verify($str_to_verify, $hash)); + } + + +} diff --git a/inc/Hura8/System/Model/DomainModel.php b/inc/Hura8/System/Model/DomainModel.php new file mode 100644 index 0000000..11f01f4 --- /dev/null +++ b/inc/Hura8/System/Model/DomainModel.php @@ -0,0 +1,153 @@ +create([ + 'domain' => $clean_domain, + 'lang' => $language, + ]); + } + + + protected function getInfoByDomain($clean_domain){ + $query = $this->db->runQuery( + "SELECT * from ".$this->tb_entity." WHERE `domain` = ? LIMIT 1 ", + ['s'], [ $clean_domain ] + ); + + return $this->db->fetchAssoc($query); + } + + + public function deleteDomain($clean_domain){ + $domain_info = $this->getInfoByDomain($clean_domain); + if($domain_info) { + $this->delete($domain_info['id']); + } + + return true; + } + + + public function setDomainMain($domain, $language){ + + $this->db->update( + $this->tb_entity, + [ "is_main" => 0, ], + [ + "lang" => $language, + "is_main" => 1 + ] + ); + + $this->db->update( + $this->tb_entity, + [ + "is_main" => 1, + "last_update" => CURRENT_TIME, + "last_update_by" => ADMIN_NAME, + ], + [ + "domain" => $domain, + ] + ); + + return true; + } + + + public function setDomainLayout($domain, $layout = 'pc'){ + $this->db->update( + $this->tb_entity, + [ "layout" => $layout, ], + [ "domain" => $domain, ] + ); + + return true; + } + + + protected function _buildQueryConditionExtend(array $condition) : ?array + { + $where_condition = ""; + $bind_types = []; + $bind_values = []; + + if(isset($condition['language']) && $condition['language']) { + $where_condition = " AND `lang` = ? "; + $bind_types[] = 's'; + $bind_values[] = $condition['language']; + } + + return [ + $where_condition, + $bind_types, + $bind_values, + ]; + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + $info = $input_info; + + // if domain exist + if($this->getInfoByDomain($info['domain'])) { + return new AppResponse('error', "Domain exist"); + } + + $info['create_time'] = CURRENT_TIME; + $info['create_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + protected function afterCreateItem($new_item_id, $new_item_info) + { + // TODO: Implement afterCreateItem() method. + } + + protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info) : AppResponse + { + $info = $new_input_info; + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) + { + // TODO: Implement afterUpdateItem() method. + } + + protected function beforeDeleteItem($item_id, $item_info) : AppResponse + { + return new AppResponse('ok'); + } + + protected function afterDeleteItem($item_id, $item_info) + { + // TODO: Implement afterDeleteItem() method. + } +} diff --git a/inc/Hura8/System/Model/EntityLanguageModel.php b/inc/Hura8/System/Model/EntityLanguageModel.php new file mode 100644 index 0000000..edcd454 --- /dev/null +++ b/inc/Hura8/System/Model/EntityLanguageModel.php @@ -0,0 +1,456 @@ +entity_type = $entity_type; + $this->tb_entity = $this->inferTbEntity($tb_entity); + $this->tb_entity_lang = $this->tb_entity."_lang"; + + $this->language_fields = (Config::getEntityLanguageFields())[$this->tb_entity] ?? []; + if(!sizeof($this->language_fields)) { + die("No language_fields found for ".$this->tb_entity." on file: config/client/language_fields.php"); + } + + $this->allow_richtext_fields = $allow_richtext_fields; + $this->db = get_db('', ENABLE_DB_DEBUG); + } + + + protected function inferTbEntity(string $tb_entity = '') : string + { + if(!$tb_entity) { + // auto infer + $tb_name = str_replace("-", "_", preg_replace("/[^a-z0-9_\-]/i", "", $this->entity_type)); + return "tb_".strtolower($tb_name); + } + + return $tb_entity; + } + + + // set language to be used by model + public function setLanguage($language) : bool + { + $this->language = $language; + return true; + } + + + public function createTableLang(): AppResponse { + $result = $this->db->checkTableExist($this->tb_entity_lang); + + // check if table exist + if(!$result) { + $this->db->runQuery("CREATE TABLE `".$this->tb_entity_lang."` LIKE `".$this->tb_entity_lang_template."` "); + + // recheck + $result = $this->db->checkTableExist($this->tb_entity_lang); + } + + return new AppResponse($result ? 'ok': 'error'); + } + + + public function recreateTableLang() { + if($this->db->checkTableExist($this->tb_entity_lang)) { + $this->db->runQuery("DROP TABLE `".$this->tb_entity_lang."` "); + } + $this->db->runQuery("CREATE TABLE `".$this->tb_entity_lang."` LIKE `".$this->tb_entity_lang_template."` "); + } + + + // any fields to have language: title, summary, description, price, etc... + public function setLanguageFields(array $language_fields) { + $this->language_fields = $language_fields; + } + + + public function getLanguageFields() : array { + return $this->language_fields; + } + + + protected function beforeCreateItem(array $input_info) { + $info = []; + foreach ($input_info as $key => $value) { + + if(!in_array($key, $this->language_fields)) continue; + + $info[$key] = $value; + } + + return (sizeof($info) > 0) ? $info : false; + } + + protected function beforeUpdateItem($current_item_info, $new_input_info) { + $info = []; + $has_change = false; + foreach ($new_input_info as $key => $value) { + if(!in_array($key, $this->language_fields)) continue; + + if(!isset($current_item_info[$key]) || $current_item_info[$key] != $value) { + $has_change = true; + } + + $info[$key] = $value; + } + + + return ($has_change) ? $info : false; + } + + public function getEntityType() : string + { + return $this->entity_type; + } + + public function getTranslatedIds(): array + { + $query = $this->db->runQuery( + "SELECT `item_id` FROM `".$this->tb_track_entity_lang."` WHERE `item_type` = ? AND `lang` = ? LIMIT 50000", + ['s', 's'], + [ $this->entity_type, $this->language] + ); + + return array_map(function ($item) { + return $item['item_id']; + }, $this->db->fetchAll($query)); + + } + + /** + * @description prevent xss attack by stripping all html tags from un-permitted fields (which mostly are) + * - only fields in ::allow_richtext_fields can keep html tags. Example: description + * - if value is array (example data=['title' => '', 'description' => html tags ]) ... + then to keep `description`, both `data` and `description` must be in the ::allow_richtext_fields + because this method checks on array value recursively + * @param array $input_info + * @return array + */ + protected function cleanRichtextFields(array $input_info) : array { + $cleaned_info = []; + foreach ($input_info as $key => $value) { + if(in_array($key, $this->allow_richtext_fields)) { + $cleaned_info[$key] = (is_array($value)) ? $this->cleanRichtextFields($value) : $value; + }else{ + $cleaned_info[$key] = (is_array($value)) ? $this->cleanRichtextFields($value) : strip_tags($value); + } + } + + return $cleaned_info; + } + + public function update($id, array $new_input_info, $search_keyword = "") : AppResponse + { + + $new_input_info = $this->cleanRichtextFields($new_input_info); + + // clean url_index if any + if(isset($new_input_info['url_index']) && $new_input_info['url_index']) { + $new_input_info['url_index'] = UrlManagerController::create_url_index($new_input_info['url_index']); + + // create url for this language if url_index setup + if(in_array('url_index', $this->language_fields) && isset($new_input_info['url_index'])) { + $request_path = $this->updateUrl($id, $new_input_info['url_index']); + $new_input_info['request_path'] = $request_path; + + // request_path must be auto allowed in language_fields if url_index in there. If not, update the language_fields + // so the beforeCreateItem method can accept it + // request_path is needed for PublicEntityBaseControllerTraits to build public urls for items in when view the language + if(!in_array('request_path', $this->language_fields)){ + $this->setLanguageFields(array_merge($this->language_fields, ['request_path'])); + } + } + } + + + $current_info = $this->getInfo($id); + + if(!$current_info) { + + $data = $this->beforeCreateItem($new_input_info); + if(!$data) { + //debug_var($new_input_info); + //die("EntityLanguageModel ".$this->entity_type.": beforeCreateItem no data"); + return new AppResponse("error", "EntityLanguageModel ".$this->entity_type.": beforeCreateItem no data"); + } + + // create + $this->db->insert( + $this->tb_entity_lang, + array( + "id" => $id, + "lang" => $this->language, + "data" => $data, + "create_time" => CURRENT_TIME, + "create_by" => ADMIN_NAME, + "last_update" => CURRENT_TIME, + "last_update_by" => ADMIN_NAME, + ) + ); + + // track item + $this->db->insert( + $this->tb_track_entity_lang, + array( + "item_type" => $this->entity_type, + "item_id" => $id, + "lang" => $this->language, + "create_time" => CURRENT_TIME, + "create_by" => ADMIN_NAME, + ) + ); + + return new AppResponse("ok"); + } + + // update + $new_change_info = $this->beforeUpdateItem($current_info, $new_input_info); + if(!$new_change_info) { + return new AppResponse("error", "EntityLanguageModel ".$this->entity_type.": Nothing changed"); + } + + + $updated_info = [ + "data" => array_merge($current_info, $new_change_info), // make sure update 1 key won't affect the other keys in the language + "last_update" => CURRENT_TIME, + "last_update_by" => ADMIN_NAME, + ]; + + $result = $this->db->update( + $this->tb_entity_lang, + $updated_info, + array( + "id" => $id, + "lang" => $this->language, + ) + ); + + return new AppResponse("ok", null, $result); + } + + + protected function updateUrl($id, $lang_url_index) { + + /* + Steps: + - infer entity model -> get default url setting (url_type, request_path, ...) + - find RequestPathConfig for entity + - create RequestPath for this language + - insert/update in tb-url + + 'url_type' => $url_type, + 'request_path' => $request_path , + 'id_path' => $id_path, + 'redirect_code' => $redirect_code, + */ + + $baseModel = $this->createEntityBaseModelInstance(); + $base_info = $baseModel->getInfo($id); + + if(!$base_info || !$base_info['request_path']) { + return false; + } + + //debug_var($base_info['request_path']); + + $objUrlManager = new UrlManagerController(); + $item_url_info = $objUrlManager->getUrlInfoByRequestPath($base_info['request_path']); + + //debug_var($item_url_info); + + $url_module = $item_url_info['module'] ; + $url_view = $item_url_info['view'] ; + + $module_routing = ModuleManager::getModuleRouting($url_module); + $request_path_config = isset($module_routing[$url_view]) ? $module_routing[$url_view]['url_manager']['request_path'] : ''; + + if(!$request_path_config) { + return false; + } + + $request_path = UrlManagerController::translateRequestPathConfig($request_path_config, $id, $lang_url_index); + + if($request_path == $item_url_info['request_path']) { + return false; + } + + $id_path = $item_url_info['id_path']."/_l:".$this->language; + + $objUrlManager->createUrl($item_url_info['url_type'], $request_path, $id_path, 0); + + return $request_path; + } + + + protected function createEntityBaseModelInstance() : iEntityModel + { + $class = EntityType::getModelClass($this->entity_type); + if(class_exists($class)) { + return _init_class($class); + } + + die($class." not exist!"); + } + + + // delete all languages for items + public function deleteAll($id): AppResponse + { + + $this->db->runQuery( + "DELETE FROM `".$this->tb_entity_lang."` WHERE `id` = ? ", + ['d'], + [$id] + ); + + $this->db->runQuery( + "DELETE FROM `".$this->tb_track_entity_lang."` WHERE `item_type` = ? AND `item_id` = ? ", + ['s', 's'], + [ $this->entity_type, $id] + ); + + return new AppResponse("ok"); + } + + + public function delete($id): AppResponse + { + + $result = $this->db->runQuery( + "DELETE FROM `".$this->tb_entity_lang."` WHERE `id` = ? AND `lang` = ? LIMIT 1 ", + ['d', 's'], + [$id, $this->language], true + ); + + if($result) { + $this->db->runQuery( + "DELETE FROM `".$this->tb_track_entity_lang."` WHERE `item_type` = ? AND `item_id` = ? AND `lang` = ? LIMIT 1 ", + ['s', 's', 's'], + [ $this->entity_type ,$id, $this->language], + true + ); + } + + return new AppResponse("ok", null, $result); + } + + + public function getListByIds(array $list_id): array + { + + if(!sizeof($list_id)) return array(); + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($list_id, 'int'); + + $bind_types[] = "s"; + $bind_values = $list_id; + $bind_values[] = $this->language; + + $query = $this->db->runQuery( + " SELECT * FROM ".$this->tb_entity_lang." WHERE `id` IN (".$parameterized_ids.") AND `lang` = ? ", + $bind_types, + $bind_values + ); + + $item_list = []; + foreach ($this->db->fetchAll($query) as $item) { + $item_list[$item['id']] = $this->formatItemInfo($item); + } + + return $item_list; + } + + + public function getInfo($id) : ?array + { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_entity_lang."` WHERE `id` = ? AND `lang` = ? LIMIT 1 ", + ['d', 's'], + [$id, $this->language] + ) ; + + if( $item_info = $this->db->fetchAssoc($query)){ + return $this->formatItemInfo($item_info); + } + + return null; + } + + + protected function formatItemInfo(array $item_info) : array + { + return ($item_info['data']) ? \json_decode($item_info['data'], true) : []; + } + + + public function getTotal(array $condition): int + { + $query = $this->db->runQuery( + " SELECT COUNT(*) AS total FROM `".$this->tb_entity_lang."` WHERE `lang` = ? " , + ['s'], [ $this->language ] + ); + $total = 0; + if ($rs = $this->db->fetchAssoc($query)) { + $total = $rs['total']; + } + + return $total; + } + + + // get empty/default item for form + public function getEmptyInfo() : array { + $empty_info = [ + "id" => 0, + "lang" => $this->language, + "create_time" => 0, + "create_by" => "", + "last_update" => 0, + "last_update_by" => "", + ]; + foreach ($this->language_fields as $field) { + $empty_info[$field] = ''; + } + + return $empty_info; + } + + +} diff --git a/inc/Hura8/System/Model/RelationModel.php b/inc/Hura8/System/Model/RelationModel.php new file mode 100644 index 0000000..9e7ccbb --- /dev/null +++ b/inc/Hura8/System/Model/RelationModel.php @@ -0,0 +1,341 @@ +db = get_db('', ENABLE_DB_DEBUG); + $this->main_item_type = $main_item_type; + $this->main_item_id = $main_item_id; + } + + + //given related_type and type_id, get all the main item ids + public function getMainItems($related_item_type, $related_item_id = 0, array $condition = [], $return_type = 'list') { + + $where_clause = " AND `main_item_type` = '" . $this->db->escape($this->main_item_type) . "' + AND `main_item_id` = '".intval($this->main_item_id)."' "; + + $where_clause .= " AND `related_item_type` = '" . $this->db->escape($related_item_type) . "' "; + + if($related_item_id) $where_clause .= " AND `related_item_id` = '".intval($related_item_id)."' "; + + //excluded_ids + if(isset($condition["excluded_ids"]) && $condition["excluded_ids"] ){ + $list_ids = DataClean::makeListOfInputSafe(explode(",", $condition["excluded_ids"]), DataType::INTEGER); + + if(sizeof($list_ids)) $where_clause .= " AND `related_item_id` NOT IN (".join(',', $list_ids ).") "; + } + + //included_ids + if(isset($condition["included_ids"]) && $condition["included_ids"] ){ + $list_ids = DataClean::makeListOfInputSafe(explode(",", $condition["included_ids"]), DataType::INTEGER); + if(sizeof($list_ids)) $where_clause .= " AND `related_item_id` IN (".join(',', $list_ids ).") "; + } + + + if($return_type == 'total') { + + $query = $this->db->query(" + SELECT COUNT(*) as total FROM `".$this->tb_relation."` + WHERE 1 " . $where_clause . " "); + + $total = 0; + if ($rs = $this->db->fetchAssoc($query)) { + $total = $rs['total']; + } + + return $total; + + } else { + + $page = isset($condition['page']) ? intval($condition['page']) : 1; + $numPerPage = isset($condition['numPerPage']) ? intval($condition['numPerPage']) : 10; + + $query = $this->db->query(" + SELECT `related_item_id` FROM `". $this->tb_relation ."` + WHERE 1 " . $where_clause . " + ORDER BY `ordering` DESC, `id` DESC + LIMIT " . ($page - 1) * $numPerPage . ", ". $numPerPage ." + "); + + $result = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + $result[] = $info['related_item_id']; + } + + return $result; + } + } + + public function updateOrdering($related_item_id, $new_order) { + + $this->db->runQuery( + "UPDATE `".$this->tb_relation."` SET + `ordering` = ? + WHERE `main_item_type` = ? AND `main_item_id` = ? AND `related_item_id` = ? ", + ['d', 's', 'd', 'd'], + [ $new_order, $this->main_item_type, $this->main_item_id, $related_item_id ] + ); + + return [ + 'status' => 'success', + 'message' => '' + ]; + } + + //@warn: this does not check if records exist. + public function create(array $related_items, $both_way_relation = true) { + + $build_insert = array(); + foreach ($related_items as $item) { + if(!$this->checkExist($this->main_item_type, $this->main_item_id, $item['type'], $item['id'])) { + $build_insert[] = [ + "main_item_type" => $this->main_item_type, + "main_item_id" => intval($this->main_item_id), + "related_item_type" => $item['type'], + "related_item_id" => intval($item['id']), + "create_time" => CURRENT_TIME, + "create_by" => ADMIN_NAME, + ]; + } + + //if 2-way relation, create item->main + if($both_way_relation) { + if(!$this->checkExist($item['type'], $item['id'], $this->main_item_type, $this->main_item_id)) { + + $build_insert[] = [ + "main_item_type" => $item['type'], + "main_item_id" => intval($item['id']), + "related_item_type" => $this->main_item_type, + "related_item_id" => intval($this->main_item_id), + "create_time" => CURRENT_TIME, + "create_by" => ADMIN_NAME, + ]; + + } + } + } + + if(sizeof($build_insert)) { + $this->db->bulk_insert($this->tb_relation, $build_insert); + + return [ + 'status' => 'success', + 'message' => '' + ]; + } + + return [ + 'status' => 'error', + 'message' => 'Đã tồn tại' + ]; + } + + + public function checkExist($main_item_type, $main_item_id, $related_item_type, $related_item_id) { + + // itself + if($main_item_type == $related_item_type && $main_item_id == $related_item_id) { + return true; + } + + $query = $this->db->runQuery( + "SELECT id FROM `".$this->tb_relation."` + WHERE `main_item_type` = ? AND `main_item_id` = ? AND `related_item_type` = ? AND `related_item_id` = ? + LIMIT 1" , + [ + 's', 'd', 's', 'd' + ], + [ + $main_item_type, + intval($main_item_id), + $related_item_type, + intval($related_item_id) + ] + ); + + if($rs = $this->db->fetchAssoc($query)) { + return $rs['id']; + } + + return false; + } + + //remove a related-item + public function remove($related_item_type, $related_item_id, $remove_both_way = true) { + + $main_item_query = ( $this->main_item_id ) ? " `main_item_id` = '".intval($this->main_item_id)."' AND " : ""; + + $this->db->query("DELETE FROM `".$this->tb_relation."` WHERE + `main_item_type` = '" . $this->db->escape($this->main_item_type) . "' AND + ". $main_item_query ." + `related_item_type` = '".$this->db->escape($related_item_type)."' AND + `related_item_id` = '".intval($related_item_id)."' "); + + if($remove_both_way) { + + $related_item_query = ( $this->main_item_id ) ? " `related_item_id` = '".intval($this->main_item_id)."' AND " : ""; + + $this->db->query("DELETE FROM `".$this->tb_relation."` WHERE + `related_item_type` = '" . $this->db->escape($this->main_item_type) . "' AND + " . $related_item_query . " + `main_item_type` = '".$this->db->escape($related_item_type)."' AND + `main_item_id` = '".intval($related_item_id)."' "); + } + + return [ + 'status' => 'success', + 'message' => '' + ]; + } + + + //remove all relate items + public function truncate() { + $this->db->runQuery( + "DELETE FROM `".$this->tb_relation."` WHERE `main_item_type` = ? AND `main_item_id` = ? ", + [ 's', 's' ], + [ $this->main_item_type, $this->main_item_id ] + ); + } + + + //count related items + public function getRelatedItemCount() { + + $query = $this->db->runQuery( + " + SELECT `related_item_type`, COUNT(`related_item_id`) AS total FROM `". $this->tb_relation ."` + WHERE `main_item_type` = ? AND `main_item_id` = ? + GROUP BY `related_item_type` + ", + [ 's', 's' ], [ $this->main_item_type, $this->main_item_id ] + ); + $result = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + $result[$info['related_item_type']] = $info['total']; + } + + return $result; + } + + + //extend getRelatedItems to get for list of main_item_ids + public function getRelatedItemsForList(array $main_item_list_ids, array $related_item_types = []) { + + if(!sizeof($main_item_list_ids)) return []; + + $bind_values = $main_item_list_ids; + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($main_item_list_ids, 'int'); + $condition = " AND `main_item_id` IN (". $parameterized_ids .") "; + + $bind_types[] = 's'; + $bind_values[] = $this->main_item_type; + $condition .= " AND `main_item_type` = ? "; + + if(sizeof($related_item_types)) { + $type_condition = []; + foreach ($related_item_types as $_type) { + $type_condition[] = " `related_item_type` = ? "; + + $bind_types[] = 's'; + $bind_values[] = $_type; + } + + $condition .= " AND ( ".join(' OR ', $type_condition )." ) "; + } + + $query = $this->db->runQuery(" + SELECT + `main_item_id` , + `related_item_type`, + `related_item_id`, + `ordering` + FROM `". $this->tb_relation ."` + WHERE 1 ". $condition ." + ORDER BY `ordering` DESC, `id` DESC + LIMIT 5000 + ", $bind_types, $bind_values); + + $result = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + $result[$info['main_item_id']][$info['related_item_type']][$info['related_item_id']] = [ + "item_id" => $info['related_item_id'], + "ordering" => $info['ordering'], + ]; + } + + //final result + $final_result = []; + foreach ($main_item_list_ids as $_id) { + $final_result[$_id] = (isset($result[$_id])) ? $result[$_id] : false; + } + + return $final_result; + } + + //get all related items and group them by type + public function getRelatedItems(array $related_item_types = []) { + + $bind_types = ['s', 's']; + $bind_values = [$this->main_item_type, $this->main_item_id]; + + $condition = ""; + if(sizeof($related_item_types)) { + $type_condition = []; + foreach ($related_item_types as $_type) { + $type_condition[] = " `related_item_type` = ? "; + + $bind_types[] = 's'; + $bind_values[] = $_type; + } + + $condition .= " AND ( ".join(' OR ', $type_condition )." ) "; + } + + + $query = $this->db->runQuery(" + SELECT + `related_item_type`, + `related_item_id`, + `ordering` + FROM `". $this->tb_relation ."` + WHERE `main_item_type` = ? AND `main_item_id` = ? ". $condition ." + ORDER BY `ordering` DESC, `id` DESC + LIMIT 5000 + ", $bind_types, $bind_values ); + $result = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + $result[$info['related_item_type']][$info['related_item_id']] = [ + "item_id" => $info['related_item_id'], + "ordering" => $info['ordering'], + ]; + } + + //check if we get only single type, then return all the results + if(sizeof($related_item_types) == 1) { + $related_item_type = $related_item_types[0]; + return isset($result[$related_item_type]) ? $result[$related_item_type] : array(); + } + + return $result; + } + + +} diff --git a/inc/Hura8/System/Model/SettingModel.php b/inc/Hura8/System/Model/SettingModel.php new file mode 100644 index 0000000..c03cb42 --- /dev/null +++ b/inc/Hura8/System/Model/SettingModel.php @@ -0,0 +1,173 @@ + 'pcbuilder_config', + ]; + + + public function __construct(){ + $this->db = get_db('', ENABLE_DB_DEBUG); + } + + + public function getAll(){ + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_setting."` WHERE 1 ORDER BY `id` DESC LIMIT 100 " + ); + + $result = []; + foreach ( $this->db->fetchAll($query) as $rs ){ + if($rs['setting_value']) { + $rs['setting_value'] = \unserialize($rs['setting_value']); + } + + $result[] = $rs; + } + + return $result; + } + + + // get a list of keys + public function getList(array $keys){ + $conditions = []; + $bind_types = []; + $bind_values = []; + + foreach ($keys as $key) { + $conditions[] = " `setting_key`= ? "; + $bind_types[] = 's'; + $bind_values[] = $key; + } + + if(sizeof($conditions)) { + $query = $this->db->runQuery( + " SELECT `setting_key`, `setting_value` FROM `".$this->tb_setting."` WHERE ".join(" OR ", $conditions)." LIMIT 100 ", + $bind_types, $bind_values + ); + + $result = []; + foreach ( $this->db->fetchAll($query) as $rs ){ + $result[$rs['setting_key']] = \unserialize($rs['setting_value']); + } + + return $result; + } + + return []; + } + + + // get a single key, = false if not found and no default is set + public function get($key, $default = null){ + $key = $this->cleanKey($key); + if(!$key) return null; + + $query = $this->db->runQuery( + "SELECT `setting_value` FROM `".$this->tb_setting."` WHERE `setting_key`= ? LIMIT 1 ", + ['s'], [ $key ] + ); + + if($rs = $this->db->fetchAssoc($query)){ + return \unserialize($rs['setting_value']); + } + + return ($default) ?: null; + } + + + public function delete($key){ + return $this->db->runQuery( + "DELETE FROM `" . $this->tb_setting . "` WHERE `setting_key` = ? LIMIT 1 ", + ['s'], [ $key ] + ); + } + + + // update or create + public function updateOrCreate($key, $value = null, $comment = ''){ + $key = $this->cleanKey($key); + if(!$key) return false; + + if($this->checkKeyExist($key)) { + return $this->db->update( + $this->tb_setting , + [ + 'setting_value' => ($value) ? \serialize($value) : null, + 'comment' => subString($comment, 80), + 'last_update' => CURRENT_TIME, + 'last_update_by' => ADMIN_NAME, + ], + [ + 'setting_key' => $key, + ] + ); + } + + return $this->db->insert( + $this->tb_setting , + [ + 'setting_key' => $key, + 'setting_value' => ($value) ? \serialize($value) : null, + 'comment' => subString($comment, 80), + 'create_time' => CURRENT_TIME, + 'create_by' => ADMIN_NAME, + 'last_update' => CURRENT_TIME, + 'last_update_by' => ADMIN_NAME, + ] + ); + } + + + public function populateKeys(array $key_list) { + $build_inserts = []; + foreach ($key_list as $key) { + if( ! $this->checkKeyExist($key)) { + $build_inserts[] = [ + "setting_key" => $key, + "create_time" => CURRENT_TIME, + "create_by" => 'system', + ]; + } + } + + if(sizeof($build_inserts)) { + $this->db->insert($this->tb_setting, $build_inserts); + } + + return sizeof($build_inserts); + } + + + protected function checkKeyExist($key) : bool { + $key = $this->cleanKey($key); + if(!$key) return false; + + $query = $this->db->runQuery( + " SELECT `id` FROM `".$this->tb_setting."` WHERE `setting_key`= ? LIMIT 1 ", ['s'], [ $key ] + ); + + return (bool) $this->db->fetchAssoc($query); + } + + + // only allow some characters + protected function cleanKey($key) { + return DataClean::makeInputSafe($key, DataType::ID); + } + +} diff --git a/inc/Hura8/System/Model/UrlModel.php b/inc/Hura8/System/Model/UrlModel.php new file mode 100644 index 0000000..058c679 --- /dev/null +++ b/inc/Hura8/System/Model/UrlModel.php @@ -0,0 +1,216 @@ +db->runQuery( + "SELECT * FROM `".$this->tb_url."` WHERE `id_path` = ? LIMIT 1 ", + ['s'], [ $id_path ] + ); + + return $this->db->fetchAssoc($query); + } + + + public function deleteByType(string $url_type) { + $this->db->runQuery( + "DELETE FROM `".$this->tb_url."` WHERE `url_type` = ? ", + ['s'], [ $url_type ] + ); + } + + + public function deleteByRequestPath($request_path) { + $this->db->runQuery( + "DELETE FROM `".$this->tb_url."` WHERE `request_path_index` = ? LIMIT 1 ", + ['s'], [ md5($request_path) ] + ); + } + + + public function getUrlByRequestPath($request_path) { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_url."` WHERE `request_path_index` = ? LIMIT 1 ", + ['s'], [ md5($request_path) ] + ); + + return $this->db->fetchAssoc($query); + } + + + //02-12-2015 + public function getUrlMetaInfo($request_path){ + return $this->db->getItemInfo($this->tb_url_meta, md5($request_path), 'request_path_index'); + } + + + public function getUrlMetaInfoById($id){ + return $this->db->getItemInfo($this->tb_url_meta, $id, 'id'); + } + + + public function getEmptyUrlMetaInfo() { + return array( + 'id' => 0, + 'request_path' => '', + 'h1' => '', + 'meta_title' => '', + 'meta_keyword' => '', + 'meta_description' => '', + 'og_image' => '', + 'summary' => '', + 'description' => '', + ); + } + + + //02-12-2015 + public function createUrlMeta(array $info){ + + //prevent duplication + if($this->getUrlMetaInfo($info['request_path'])) return false; + + if(!defined("ADMIN_NAME")) define("ADMIN_NAME", "system"); + + $copy = $info; + $copy['request_path_index'] = md5($info['request_path']); + $copy['last_update'] = CURRENT_TIME; + $copy['last_update_by'] = ADMIN_NAME; + + return $this->db->insert( + $this->tb_url_meta , + $copy + ); + } + + + public function updateUrlMeta($id, array $info){ + $copy = $info; + $copy['last_update'] = CURRENT_TIME; + $copy['last_update_by'] = ADMIN_NAME; + + return $this->db->update( + $this->tb_url_meta , + $copy, + [ + 'id' => $id, + ] + ); + } + + + protected function _buildQueryConditionExtend(array $condition): ?array + { + + /*$condition = array( + "letter" => "", + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + //loc theo is_redirect + if(isset($condition["request_path"]) && $condition["request_path"]) { + $path = preg_replace("/[^a-z0-9_\.\/\-]/i", '', $condition["request_path"]); + if($path) $catCondition[] = " AND `request_path` LIKE '%".$path."%' "; + } + + //loc theo is_redirect + if(isset($condition["is_redirect"]) && $condition["is_redirect"]) { + if($condition["is_redirect"] == 1) $catCondition[] = " AND `url_type` = 'redirect' "; + else if($condition["is_redirect"] == -1) $catCondition[] = " AND `url_type` != 'redirect' "; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function beforeCreateItem(array $input_info): AppResponse + { + $request_path = $input_info['request_path'] ?? ''; + $id_path = $input_info['id_path'] ?? ''; + + if(!$request_path || $this->getUrlByRequestPath($request_path)) { + return new AppResponse('error', "request_path exist"); + } + + if($id_path && $this->getUrlByIdPath($id_path)) { + return new AppResponse('error', "id path exist"); + } + + $admin_name = (defined("ADMIN_NAME")) ? ADMIN_NAME : "system"; + + $info = $input_info; + + $info['request_path_index'] = md5($info['request_path']); + + $info['create_time'] = CURRENT_TIME; + $info['create_by'] = $admin_name; + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = $admin_name; + + return new AppResponse('ok', null, $info); + } + + + protected function afterCreateItem($new_item_id, $new_item_info) + { + // TODO: Implement afterCreateItem() method. + } + + + protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info): AppResponse + { + $info = $new_input_info; + unset($info['id_path']); + + if(isset($info['request_path']) && $info['request_path']) { + $info['request_path_index'] = md5($info['request_path']); + } + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) + { + // TODO: Implement afterUpdateItem() method. + } + + + protected function beforeDeleteItem($item_id, $item_info): AppResponse + { + return new AppResponse('ok'); + } + + + protected function afterDeleteItem($item_id, $item_info) + { + // TODO: Implement afterDeleteItem() method. + } + + + protected function extendedFilterOptions(): array + { + return []; + } +} diff --git a/inc/Hura8/System/Model/UtilityModel.php b/inc/Hura8/System/Model/UtilityModel.php new file mode 100644 index 0000000..6470a66 --- /dev/null +++ b/inc/Hura8/System/Model/UtilityModel.php @@ -0,0 +1,39 @@ +db = get_db($db_id, ENABLE_DB_DEBUG); + } + + + public function createTableLang($tb_entity_lang): AppResponse { + $result = $this->db->checkTableExist($tb_entity_lang); + + // check if table exist + if(!$result) { + $this->db->runQuery("CREATE TABLE `".$tb_entity_lang."` LIKE `".$this->tb_entity_lang_template."` "); + + // recheck + $result = $this->db->checkTableExist($tb_entity_lang); + } + + return new AppResponse($result ? 'ok': 'error'); + } + + +} diff --git a/inc/Hura8/System/Model/WebUserModel.php b/inc/Hura8/System/Model/WebUserModel.php new file mode 100644 index 0000000..97c6c6e --- /dev/null +++ b/inc/Hura8/System/Model/WebUserModel.php @@ -0,0 +1,92 @@ +db = get_db('', ENABLE_DB_DEBUG); + $this->user_browser_id = preg_replace("/[^a-z0-9]/i", "", $user_browser_id); + $this->user_db_id = (defined("USER_ID")) ? USER_ID : 0; + } + + public function getValue($key) { + $key = $this->cleanKey($key); + + $query = $this->db->runQuery("SELECT `content` FROM `".$this->tb_user."` + WHERE `user_id` = ? AND `key` = ? + LIMIT 1 ", ['s', 's'], [ $this->user_browser_id , $key ]) ; + if($rs = $this->db->fetchAssoc($query )){ + return \unserialize($rs['content']); + } + + return null; + } + + public function setValue($key, $value) { + $key = $this->cleanKey($key); + + if($row_id = $this->checkKey($key)) { + return $this->db->update( + $this->tb_user , + [ + 'content' => \serialize($value) , + 'user_db_id' => $this->user_db_id , + 'last_update' => CURRENT_TIME , + ], + [ + 'id' => $row_id, + ] + ); + + } + + return $this->db->insert( + $this->tb_user , + [ + 'user_id' => $this->user_browser_id , + 'user_db_id' => $this->user_db_id, + 'key' => $key, + 'content' => \serialize($value), + 'create_time' => CURRENT_TIME , + 'last_update' => CURRENT_TIME, + ] + ); + } + + //delete all user history + public function deleteUser($key) { + $this->db->runQuery( + "DELETE FROM `".$this->tb_user."` WHERE `user_id` = ? LIMIT 1 ", + ['s'], + [ $this->user_browser_id ] + ) ; + } + + protected function cleanKey($key) { + return preg_replace("/[^a-z0-9]/i", "", $key); + } + + protected function checkKey($key) { + $query = $this->db->runQuery("SELECT `id` FROM `".$this->tb_user."` + WHERE `user_id` = ? AND `key` = ? + LIMIT 1", ['s', 's'], [ $this->user_browser_id , $key ] ) ; + if($rs = $this->db->fetchAssoc($query )){ + return $rs['id']; + } + + return false; + } +} diff --git a/inc/Hura8/System/Model/aCategoryBaseModel.php b/inc/Hura8/System/Model/aCategoryBaseModel.php new file mode 100644 index 0000000..5540e59 --- /dev/null +++ b/inc/Hura8/System/Model/aCategoryBaseModel.php @@ -0,0 +1,292 @@ +array(childId) + foreach ( $this->getAll($condition) as $item ) { + $item_list[$item['parent_id']][$item['id']] = $item; + } + + return $item_list; + } + + + // get all categories + public function getAll(array $condition = array()){ + /*$condition = [ + "parent_id" => 1, + "status" => 1, + ];*/ + + $bind_types = []; + $bind_values = []; + $where_clause = ''; + + if(isset($condition['status'])) { + $where_clause .= " AND `status` = ? "; + $bind_types[] = 'd'; + $bind_values[] = intval($condition['status']); + } + + $query = $this->db->runQuery( + " SELECT * FROM `". $this->tb_entity ."` WHERE 1 ".$where_clause." ORDER BY `ordering` DESC LIMIT 5000 ", + $bind_types, $bind_values + ); + + return $this->db->fetchAll($query); + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + $parent_id = isset($input_info['parent_id']) ? $input_info['parent_id'] : 0; + $api_key = (isset($input_info['api_key']) && $input_info['api_key']) ? $input_info['api_key'] : $input_info['title']; + $api_key = Security\DataClean::makeInputSafe($api_key, DataType::ID); + + $api_key = $this->createUniqueAPIKey(0, $api_key); + + if(!isset($input_info['url_index']) || !$input_info['url_index']) { + $input_info['url_index'] = $this->createUniqueUrlIndex(0, $input_info['title']); + }else{ + $input_info['url_index'] = $this->createUniqueUrlIndex(0, $input_info['url_index']); + } + + $info = array_merge($input_info, array( + "parent_id" => $parent_id, + "api_key" => $api_key, + "create_by" => ADMIN_NAME, + "create_time" => CURRENT_TIME, + "last_update_by" => ADMIN_NAME, + "last_update" => CURRENT_TIME, + ) ); + + return new AppResponse('ok', null, $info); + } + + + protected function afterCreateItem($new_item_id, $new_item_info) { + //update path&child + $this->updatePath($new_item_id); + $this->updateChild($new_item_id); + $this->updateChild($new_item_info['parent_id']); + } + + + protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info) : AppResponse + { + $info = $new_input_info; + + if(isset($info['url_index'])) { + if(!$info['url_index']) { + $info['url_index'] = $this->createUniqueUrlIndex($item_id, $info['title']); + }else{ + $info['url_index'] = $this->createUniqueUrlIndex($item_id, $info['url_index']); + } + } + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) { + //update cat-path for category + $this->updatePath($item_id); + $this->updateChild($item_id); + + //update child_ids for new/old parents and parents of parent + if($new_item_info['parent_id'] != $old_item_info['parent_id']) { + $this->updateChild($new_item_info['parent_id']); + $this->updateChild($old_item_info['parent_id']); + } + } + + + protected function afterDeleteItem($item_id, $item_info){ + $this->updateChild($item_info["parent_id"]); + } + + + //create an unique request-path + protected function createUniqueAPIKey($id, $api_key){ + + //if exist and belong other id, create a new one + $query = $this->db->runQuery("SELECT `id` FROM `".$this->tb_entity."` WHERE `api_key` = ? LIMIT 1 ", ['s'], [$api_key]) ; + if($info = $this->db->fetchAssoc($query)){ + if($info['id'] != $id) { + $new_api_key = $api_key."-1"; + return $this->createUniqueAPIKey($id, $new_api_key); + } + } + + return $api_key; + } + + + protected function updatePathAndChildAll($id, $child_ids, $parent_id, $old_parent_id) { + $this->updatePathAndChild($id); + + //update for childs + $list_child_to_update = array_filter(explode(",", $child_ids)); + foreach($list_child_to_update as $_id) { + if($_id != $id) $this->updatePathAndChild($_id); + } + + //cap nhat lai child list cua danh muc old_parent and new parent id, and parent of these parents + $query = $this->db->runQuery( + "SELECT cat_path FROM `". $this->tb_entity ."` WHERE `id`= ? OR `id`= ? ", + ['d', 'd'], [$parent_id, $old_parent_id] + ); + + $cat_path_all = join(":", array_map(function ($item){ + return $item['cat_path']; + } , $this->db->fetchAll($query))); + + + $list_parent_to_update = array_unique(array_filter(explode(":", $cat_path_all))); + + foreach($list_parent_to_update as $_id) { + if($_id > 0) $this->updatePathAndChild($_id); + } + } + + + public function updatePathAndChild($id) { + if(!$id) return false; + + $new_cat_path = $this->findCatPath($id); + $new_child_list = $this->findChildList($id); + $is_parent = ( $new_child_list === $id) ? 0 : 1; + + return $this->db->update( + $this->tb_entity , + [ + 'cat_path' => $new_cat_path, + 'child_ids' => $new_child_list, + 'is_parent' => $is_parent, + ], + [ + 'id' => $id, + ] + ); + } + + + protected function updatePath($id) { + if(!$id) return false; + + $new_cat_path = $this->findCatPath($id); + + return $this->db->update( + $this->tb_entity , + [ + 'cat_path' => $new_cat_path, + ], + [ + 'id' => $id, + ] + ); + } + + //update childs for current id and its parents and its parents' parents.... + protected function updateChild($id) { + if(!$id) return false; + + $query = $this->db->runQuery("SELECT `cat_path` FROM `".$this->tb_entity."` WHERE `id` = ? LIMIT 1 ", ['d'], [$id]) ; + + if($item_info = $this->db->fetchAssoc($query)){ + $cat_id_list = array_filter(explode(":", $item_info['cat_path'])); + + foreach ($cat_id_list as $_id) { + $new_child_list = $this->findChildList($_id); + $is_parent = ( $new_child_list === $_id) ? 0 : 1; + + $this->db->update( + $this->tb_entity , + [ + 'child_ids' => $new_child_list, + 'is_parent' => $is_parent, + ], + [ + 'id' => $_id, + ] + ); + } + } + + return true; + } + + + protected function _buildQueryConditionExtend(array $condition) : ?array + { + $catCondition = []; + $bind_types = []; + $bind_values = []; + + return array(join(" ", $catCondition), $bind_types, $bind_values); + } + + + //build category path 0:parent:categoryId + protected function findCatPath($categoryId){ + $path = ":".$categoryId; + $query = $this->db->runQuery("SELECT `parent_id` FROM `".$this->tb_entity."` WHERE `id` = ? LIMIT 1", ['d'], [$categoryId]); + if($rs = $this->db->fetchAssoc($query)){ + if($rs['parent_id']) $path .= $this->findCatPath($rs['parent_id']); + } + + return $path; + } + + //lay toan bo cac muc la con + protected function findChildList($p_category_id){ + $all_categories = $this->getAllByParent(); + + $list = $p_category_id; + if(isset($all_categories[$p_category_id])) { + foreach ( $all_categories[$p_category_id] as $rs ) { + $list .= ",". $this->findChildList($rs['id']); + } + } + + return $list; + } + + + protected function getAllParent(array $condition = []) { + $cache_key = 'all-category-by-parent-'.$this->getEntityType(); + + return self::getCache($cache_key, function () use ($condition){ + return $this->getAllByParent($condition); + }); + } + +} diff --git a/inc/Hura8/System/Model/aEntityBaseModel.php b/inc/Hura8/System/Model/aEntityBaseModel.php new file mode 100644 index 0000000..26bd886 --- /dev/null +++ b/inc/Hura8/System/Model/aEntityBaseModel.php @@ -0,0 +1,428 @@ + '', // keyword search + 'q_options' => [ + 'field_filters' => [], + 'fulltext_fields' => ['keywords'], + 'limit_result' => 2000 + ], // q_options as in iSearch->find($keyword, array $field_filters = [], array $fulltext_fields = ["keywords"], $limit_result = 2000) + 'featured' => '', // 1|-1 + 'status' => '', // 1|-1 + 'excluded_ids' => null, // [id1, id2, ...] or null + 'included_ids' => null, // [id1, id2, ...] or null + ]; + + /* @var ?iSearch $iSearchModel */ + protected $iSearchModel; + + public function __construct( + $entity_type, + $tb_entity = "", + $iSearchModel = null, + $allow_richtext_fields = [] + ) { + + $this->entity_type = $entity_type; + + $this->db = get_db('', ENABLE_DB_DEBUG); + $this->iSearchModel = $iSearchModel; + $this->allow_richtext_fields = $allow_richtext_fields; + + if($tb_entity) { + $this->tb_entity = $tb_entity; + }else{ + //auto infer + $tb_name = str_replace("-", "_", preg_replace("/[^a-z0-9_\-]/i", "", $entity_type)); + $this->tb_entity = "tb_".strtolower($tb_name); + } + } + + + /** + * @description to be extended by extending class + * besides the $base_filter_condition_keys which allows to find items in ::getList($condition) + * example: + [ + 'category' => array[], + 'brand' => array[], + ] + * @return array + */ + abstract protected function extendedFilterOptions() : array ; + + + /** + * @description utility to get all possible entity filter during module development + * @return array + */ + public function getAllowedFilterConditionKeys(): array + { + return array_merge( + $this->extendedFilterOptions(), + $this->base_filter_condition_keys + ); + } + + + public function setRichtextFields(array $allow_richtext_fields = []) { + $this->allow_richtext_fields = $allow_richtext_fields; + } + + + + + /** + * @description + * @param array $filter_condition + * @return array|null array($string_where, $bind_types, $bind_values ); + */ + protected function _buildQueryConditionExtend(array $filter_condition): ?array { + return []; + } + + + /** + * @param string $sort_by + * @return string + */ + protected function _buildQueryOrderBy(string $sort_by = "new") { + return " `id` DESC "; + } + + + /** + * @param array $item_info + * @return array + */ + protected function formatItemInList(array $item_info) : array { + return $item_info; + } + + + /** + * @param array $item_info + * @return array|null + */ + protected function formatItemInfo(array $item_info) : ?array { + return $item_info; + } + + + + public function getEntityType() : string { + return $this->entity_type; + } + + + /** + * @description prevent xss attack by stripping all html tags from un-permitted fields (which mostly are) + * - only fields in ::allow_richtext_fields can keep html tags. Example: description + * - if value is array (example data=['title' => '', 'description' => html tags ]) ... + then to keep `description`, both `data` and `description` must be in the ::allow_richtext_fields + because this method checks on array value recursively + * @param array $input_info + * @return array + */ + protected function cleanRichtextFields(array $input_info) : array { + $cleaned_info = []; + foreach ($input_info as $key => $value) { + if($value instanceof MysqlValue) { + $cleaned_info[$key] = $value; + } else { + if(in_array($key, $this->allow_richtext_fields)) { + $cleaned_info[$key] = (is_array($value)) ? $this->cleanRichtextFields($value) : $value; + }else{ + $cleaned_info[$key] = (is_array($value)) ? $this->cleanRichtextFields($value) : strip_tags($value); + } + } + } + + return $cleaned_info; + } + + + + public function getListByIds(array $list_id, array $condition = array()) : array + { + if(!sizeof($list_id)) { + return []; + } + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($list_id, 'int'); + + $where_clause = ""; + $bind_values = $list_id; + if(isset($condition['status'])) { + $where_clause .= " AND `status` = ? "; + $bind_types[] = 'd'; + $bind_values[] = intval($condition['status']); + } + + $query = $this->db->runQuery( + " SELECT * FROM ".$this->tb_entity." WHERE `id` IN (".$parameterized_ids.") ".$where_clause, + $bind_types, + $bind_values + ); + + $item_list = []; + foreach ($this->db->fetchAll($query) as $item) { + $item_list[$item['id']] = $this->formatItemInList($item); + } + + return $item_list; + } + + + public function getInfo($id) : ?array + { + $query = $this->db->runQuery("SELECT * FROM `".$this->tb_entity."` WHERE `id` = ? LIMIT 1 ", ['d'], [$id]) ; + if( $item_info = $this->db->fetchAssoc($query)){ + return $this->formatItemInfo($item_info); + } + + return null; + } + + + /** + * @description utility to inspect the actual filters which will be used in getList by Model + * @param array $condition + * @return array + */ + public function getQueryCondition(array $condition) : array + { + return $this->_buildQueryCondition($condition); + } + + + public function getTotal(array $condition) : int + { + list( $where_clause, $bind_types, $bind_values) = $this->_buildQueryCondition($condition); + + $query = $this->db->runQuery( + " SELECT COUNT(*) as total FROM `".$this->tb_entity."` WHERE 1 " . $where_clause, + $bind_types, $bind_values + ); + + $total = 0; + if ($rs = $this->db->fetchAssoc($query)) { + $total = $rs['total']; + } + + return $total; + } + + + public function getList(array $condition) : array + { + list( $where_clause, $bind_types, $bind_values) = $this->_buildQueryCondition($condition); + + //debug_var([$where_clause, $bind_types, $bind_values]); + + $numPerPage = (isset($condition['numPerPage']) && $condition['numPerPage'] > 0 ) ? intval($condition['numPerPage']) : 20 ; + $page = (isset($condition['page']) && $condition['page'] > 0 ) ? intval($condition['page']) : 1 ; + + $sort_by = (isset($condition['sort_by']) && $condition['sort_by']) ? $condition['sort_by'] : 'new' ; + $order_by = $this->_buildQueryOrderBy($sort_by); + + $query = $this->db->runQuery( + "SELECT * FROM ".$this->tb_entity." WHERE 1 ". $where_clause . " + ORDER BY ".$order_by." + LIMIT ".(($page-1) * $numPerPage).", ".$numPerPage , + $bind_types, + $bind_values + ) ; + + $item_list = array(); + $counter = ($page-1) * $numPerPage; + + foreach ( $this->db->fetchAll($query) as $item_info ) { + $counter += 1; + + $item = $item_info; + $item['counter'] = $counter; + + $item_list[] = $this->formatItemInList($item); + } + + return $item_list; + } + + + //to avoid duplicate codes, extended class should implement this method for extra filter keys + // which are not in the base filter ::_buildQueryConditionBase + protected function _buildQueryCondition(array $filter_condition) + { + // these keys are for pagination and ordering list, which are commonly included in the $filter_condition + $excluded_keys = [ + 'numPerPage', 'page', 'sort_by' + ]; + + + $base_filter_conditions = []; + $extend_filter_conditions = []; + foreach ($filter_condition as $key => $value) { + if(in_array($key, $excluded_keys)) { + continue; + } + + if(array_key_exists($key, $this->base_filter_condition_keys) ) { + $base_filter_conditions[$key] = $value; + }else{ + $extend_filter_conditions[$key] = $value; + } + } + + list($base_where, $base_bind_types, $base_bind_values) = $this->_buildQueryConditionBase($base_filter_conditions); + + list( + $extend_where, + $extend_bind_types, + $extend_bind_values + ) = $this->_buildQueryConditionExtend($extend_filter_conditions) ?: [ '', null, null]; + + + if($extend_where) { + return array( + join(" ", [$base_where, $extend_where]), + array_merge($base_bind_types, $extend_bind_types), + array_merge($base_bind_values, $extend_bind_values), + ); + } + + return array( + $base_where, + $base_bind_types, + $base_bind_values + ); + + } + + + protected function _buildQueryConditionBase(array $filter_conditions) + { + /* + $filter_conditions = [ + 'q' => '', // keyword search + 'q_options' => [ + 'field_filters' => [], + 'fulltext_fields' => ['keywords'], + 'limit_result' => 2000 + ], // q_options as in iSearch->find($keyword, array $field_filters = [], array $fulltext_fields = ["keywords"], $limit_result = 2000) + + 'featured' => '', // 1|-1 + 'status' => '', // 1|-1 + 'excluded_ids' => [], // [id1, id2, ...] or null + 'included_ids' => [], // [id1, id2, ...] or null + ]; + */ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + //Tim kiem theo tu khoa + if(isset($filter_conditions["q"]) && $filter_conditions["q"] && $this->iSearchModel ){ + + $q_options = $filter_conditions["q_options"] ?? []; + $field_filters = $q_options['field_filters'] ?? []; + $fulltext_fields = $q_options['fulltext_fields'] ?? []; + $limit_result = $q_options['limit_result'] ?? 2000; + + $match_result = $this->iSearchModel->find($filter_conditions["q"], $field_filters, $fulltext_fields, $limit_result); + + list($parameterized_ids, $sub_bind_types) = create_bind_sql_parameter_from_value_list($match_result, 'int'); + + if($parameterized_ids) { + $catCondition[] = " AND `id` IN (".$parameterized_ids.") "; + $bind_types = array_merge($bind_types, $sub_bind_types); + $bind_values = array_merge($bind_values, $match_result); + }else{ + $catCondition[] = " AND `id` = -1 "; + } + } + + //loc theo featured + if(isset($filter_conditions["featured"]) && $filter_conditions["featured"]) { + $catCondition[] = " AND `is_featured` = ? "; + $bind_types[] = 'd'; + $bind_values[] = ($filter_conditions["featured"] == 1) ? 1 : 0; + } + + //loc theo status + if(isset($filter_conditions["status"]) && $filter_conditions["status"]) { + $catCondition[] = " AND `status` = ? "; + $bind_types[] = 'd'; + $bind_values[] = ($filter_conditions["status"] == 1) ? 1 : 0; + } + + // excluded_ids + if(isset($filter_conditions['excluded_ids']) && is_array($filter_conditions['excluded_ids']) ) { + list($parameterized_ids, $sub_bind_types) = create_bind_sql_parameter_from_value_list($filter_conditions['excluded_ids'], 'int'); + + if($parameterized_ids) { + $catCondition[] = " AND `id` NOT IN (".$parameterized_ids.") "; + $bind_types = array_merge($bind_types, $sub_bind_types); + $bind_values = array_merge($bind_values, $filter_conditions['excluded_ids']); + } + } + + // included_ids + if(isset($filter_conditions['included_ids']) && is_array($filter_conditions['included_ids']) ) { + list($parameterized_ids, $sub_bind_types) = create_bind_sql_parameter_from_value_list($filter_conditions['included_ids'], 'int'); + + if($parameterized_ids) { + $catCondition[] = " AND `id` IN (".$parameterized_ids.") "; + $bind_types = array_merge($bind_types, $sub_bind_types); + $bind_values = array_merge($bind_values, $filter_conditions['included_ids']); + }else{ + $catCondition[] = " AND `id` = -1 "; + } + } + + return array( + join(" ", $catCondition), + $bind_types, + $bind_values + ); + } + + + // get empty/default item for form + public function getEmptyInfo($addition_field_value = []) : array + { + $table_info = $this->db->getTableInfo($this->tb_entity); + $empty_info = []; + foreach ($table_info as $field => $field_info) { + $empty_info[$field] = ( in_array($field_info['DATA_TYPE'], ['int', 'float', 'mediumint', 'smallint', 'tinyint']) ) ? 0 : '' ; + } + + if(sizeof($addition_field_value)) { + return array_merge($empty_info, $addition_field_value); + } + + return $empty_info; + } + + +} diff --git a/inc/Hura8/System/Model/aSearchBaseModel.php b/inc/Hura8/System/Model/aSearchBaseModel.php new file mode 100644 index 0000000..33ab607 --- /dev/null +++ b/inc/Hura8/System/Model/aSearchBaseModel.php @@ -0,0 +1,530 @@ + matches: abcd, abc, abcde + + // require rebuilding search if change this value + protected $min_star_search_length = 2; //min star word length: result: abcd -> keywords: ab, abc, abcd NOT a + + // define list of fields to be the filters + protected $config_filter_fields = [ + //format: field_name => map table_name.field + //'price' => "tb_product.price", + //'quantity' => "tb_product.quantity", + ]; + + protected $config_fulltext_fields = [ + //format: field_name => map [table_name.field] + //"product_keywords" => ["tb_product.title", "tb_product.model", "tb_product.sku"], + //"category_keywords" => ["tb_category.title"], + ]; + + + public function __construct( + $tb_main, + array $config_fulltext_fields , + array $config_filter_fields = [] + ) { + + // ovewrite default + if(defined('CONFIG_STAR_SEARCH')) $this->star_search = CONFIG_STAR_SEARCH; + if(defined('CONFIG_STAR_SEARCH_MIN_LENGTH')) $this->min_star_search_length = CONFIG_STAR_SEARCH_MIN_LENGTH; + if(defined('CONFIG_SEARCH_VIETNAMESE')) $this->search_vietnamese = CONFIG_SEARCH_VIETNAMESE; + + $this->db = get_db('', ENABLE_DB_DEBUG); + $this->config_fulltext_fields = $config_fulltext_fields; + $this->config_filter_fields = $config_filter_fields; + + $this->tb_main = $tb_main; + $part_name = str_replace("tb_", "", preg_replace("/[^a-z0-9_]/i", "", $tb_main)); + $this->tb_fulltext = "tb_search_".strtolower($part_name); + } + + /** + * @description get filter fields + * @param array[string] + */ + public function getFilterFields(): array { + return $this->config_filter_fields; + } + + + /** + * @description get fulltext fields + * @param array[string] + */ + public function getFulltextFields(): array { + return $this->config_fulltext_fields; + } + + + public function getSampleData() : array { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_fulltext."` ORDER BY `item_id` DESC LIMIT 10" + ); + + return $this->db->fetchAll($query); + } + + + public function getTableName() : string { + return $this->tb_fulltext; + } + + + /** + * @param array $field_filters ["price" => [">", 100], "brand" => ["=", 1]] + * @return array + */ + protected function _buildFieldFilters(array $field_filters = []) { + $permit_operations = [">","=","<",">=","<=", "BETWEEN", "IN"]; + $where_clause = []; + $bind_types = []; + $bind_values = []; + + foreach ($field_filters as $field => $info) { + list($operation, $value) = $info; + + $operation = strtoupper($operation); + + if(!in_array(strtolower($operation), $permit_operations)) continue; + + if($operation == 'BETWEEN') { + // $value must be array(value_int_1, value_int_2) + if(is_array($value) && sizeof($value) == 2) { + list($value_1, $value_2) = $value; + + if(is_int($value_1) && is_int($value_2) && $value_1 < $value_2) { + $where_clause[] = " AND `".$field."` BETWEEN (?, ?) "; + + $bind_types[] = "d"; + $bind_types[] = "d"; + + $bind_values[] = $value_1; + $bind_values[] = $value_2; + } + } + + continue; + } + + if($operation == 'IN') { + // $value must be array(value1, value2) + if(is_array($value) && sizeof($value) > 0) { + $parameterized = []; + foreach ($value as $_v) { + $parameterized[] = "?"; + $bind_types[] = "s"; + $bind_values[] = $_v; + } + + $where_clause[] = " AND `".$field."` IN (".join(",", $parameterized).") "; + } + + continue; + } + + + // default operation, comparison requires value to be a digit + $where_clause[] = " AND `".$field."` ".$operation." ? "; + $bind_types[] = "d"; + $bind_values[] = $value; + } + + return array(join(" ", $where_clause), $bind_types, $bind_values); + } + + + /** + * @param $keyword + * @param array $field_filters ["price" => [">", 100], "brand" => ["=", 1]] + * @param array $fulltext_fields ['title_keywords', "category_keywords"] + * @param int $limit_result + * @return int[] + */ + public function find($keyword, array $field_filters = [], array $fulltext_fields = [], $limit_result = 2000) : array + { + + if(!sizeof($fulltext_fields)) $fulltext_fields = array_keys($this->config_fulltext_fields); + + $cache_key = md5("find-".$keyword); + + return static::getCache($cache_key, function () use ($keyword, $field_filters, $fulltext_fields, $limit_result){ + + $keyword_clean = ($this->search_vietnamese) ? $this->make_text_search_vn($keyword) : $this->make_text_clean($keyword) ; //make_text_search_vn + + $keyword_clean_ele = array_filter(explode(" ", $keyword_clean)); + $build_boolean_search = "+".join(" +", $keyword_clean_ele); + + if(!$limit_result || $limit_result > 5000) $limit_result = 5000; + $limit_condition = ($limit_result > 0) ? " LIMIT ".$limit_result : ""; + + list($where_clause, $bind_types, $bind_values) = $this->_buildFieldFilters($field_filters); + + //validate search fields + $validated_fulltext_fields = array_filter($fulltext_fields, function ($item) { return array_key_exists($item, $this->config_fulltext_fields); }); + + if(!sizeof($validated_fulltext_fields)) { + return []; + } + + $query = $this->db->runQuery( + "SELECT `item_id` FROM `".$this->tb_fulltext."` + WHERE MATCH(".join(',', $validated_fulltext_fields).") AGAINST('".$build_boolean_search."' IN BOOLEAN MODE) + ".$where_clause." + ORDER BY `item_id` DESC + ".$limit_condition." + ", + $bind_types, $bind_values + ); + + return array_map(function ($item){ return $item['item_id']; }, $this->db->fetchAll($query) ); + + }); + + } + + + protected function getItemInfo($item_id) + { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_fulltext."` WHERE `item_id` = ? LIMIT 1", + ['d'], [$item_id] + ); + + return $this->db->fetchAssoc($query); + } + + + protected function checkExist($item_id) : bool + { + $query = $this->db->runQuery( + "SELECT `item_id` FROM `".$this->tb_fulltext."` WHERE `item_id` = ? LIMIT 1", + ['d'], [$item_id] + ); + + return (bool) $this->db->fetchAssoc($query); + } + + + // get all table_fields to watch which will affect the search + protected function getWatchTableFields() { + $result = []; + foreach ($this->config_filter_fields as $field => $table_field ) { + $result[] = $table_field; + } + + foreach ($this->config_fulltext_fields as $field => $table_fields ) { + $result = array_merge($result, $table_fields); + } + + return $result; + } + + + /** + * @description update or create item if not exist + * @param $item_id + * @param array $table_field_values + $table_field_values = [ + "tb_product.price" => 2000000, + "tb_product.title" => "Máy tính ABC", + "tb_category.price" => "Máy tính", + + "tb_product.field_not_be_update" => "Mmodel", + ]; + * @return AppResponse + */ + public function updateItem($item_id, array $table_field_values = []) : AppResponse + { + + $fulltext_fields = []; + foreach ($this->config_fulltext_fields as $fulltext_field => $fulltext_map_fields ) { + foreach ($fulltext_map_fields as $_table_field) { + if(array_key_exists($_table_field, $table_field_values)) { + $fulltext_fields[$fulltext_field][$_table_field] = $table_field_values[$_table_field]; + } + } + } + + //echo "fulltext_fields"; + //debug_var($fulltext_fields); + //echo "
"; + + $field_filters = []; + foreach ($this->config_filter_fields as $filter_field => $filter_map_field ) { + if(array_key_exists($filter_map_field, $table_field_values)) { + $field_filters[$filter_field] = $table_field_values[$filter_map_field]; + } + } + + //echo "field_filters"; + //debug_var($field_filters); + + // nothing to update + if(!sizeof($fulltext_fields) && !sizeof($field_filters)) { + return new AppResponse('error', "nothing to update" ); + } + + + // create entry if not exist + if(!$this->checkExist($item_id)) { + $this->db->insert($this->tb_fulltext, ['item_id' => $item_id, 'create_time' => CURRENT_TIME]); + } + + $current_info = $this->getItemInfo($item_id); + if(!$current_info) { + return new AppResponse('error', "Cannot find record for ".$item_id ); + } + + // update + $updated_fulltext = []; + + foreach ($this->config_fulltext_fields as $_filter_field => $_n ) { + if(!isset($fulltext_fields[$_filter_field])) continue; + + $current_filter_field_base = ($current_info[$_filter_field."_base"]) ? \json_decode($current_info[$_filter_field."_base"], true) : []; + + $new_filter_field_base = $current_filter_field_base; + + foreach ($fulltext_fields[$_filter_field] as $_table_field => $_new_value) { + $new_filter_field_base[$_table_field] = $_new_value; + } + + if(json_encode($new_filter_field_base) != json_encode($current_filter_field_base)) { + $updated_fulltext[$_filter_field."_base"] = $new_filter_field_base; + $updated_fulltext[$_filter_field] = ($this->search_vietnamese) ? $this->make_text_search_vn(join(" ", array_values($new_filter_field_base))) : $this->make_text_clean(join(" ", array_values($new_filter_field_base))); + + } + } + + + $updated_filters = []; + foreach ($this->config_filter_fields as $_filter_field => $_table_field ) { + if(!isset($field_filters[$_filter_field])) continue; + + if($field_filters[$_filter_field] != $current_info[$_filter_field] ) { + $updated_filters[$_filter_field] = $field_filters[$_filter_field]; + } + } + + + // nothing to update + if(!sizeof($updated_filters) && !sizeof($updated_fulltext)) { + return new AppResponse('error', "nothing to update" ); + } + + $updated_info = array_merge($updated_filters, $updated_fulltext); + $updated_info['last_update'] = CURRENT_TIME; + + //echo "updated_info =
"; + //debug_var($updated_info); + + $this->db->update( + $this->tb_fulltext, + $updated_info, + ['item_id' => $item_id] + ); + + return new AppResponse('ok'); + } + + public function deleteItem($item_id) : AppResponse + { + $this->db->runQuery( + "DELETE FROM `".$this->tb_fulltext."` WHERE `item_id` = ? LIMIT 1", + ['d'], [$item_id] + ); + + return new AppResponse('ok'); + } + + public function deleteItems(array $item_list_ids) : AppResponse + { + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($item_list_ids, "int"); + + $this->db->runQuery( + "DELETE FROM `".$this->tb_fulltext."` WHERE `item_id` IN (".$parameterized_ids.") ", + $bind_types, + $item_list_ids + ); + + return new AppResponse('ok'); + } + + public function recreateTableSearch() { + if($this->db->checkTableExist($this->tb_fulltext)) { + $this->db->runQuery( + "DROP TABLE `".$this->tb_fulltext."` " + ); + } + + $this->createTableSearch(); + } + + public function createTableSearch() { + // check if table exist + if($this->db->checkTableExist($this->tb_fulltext)) { + return true; + } + + if(!$this->db->checkTableExist($this->tb_main)) { + return false; + } + + $sql = "CREATE TABLE `".$this->tb_fulltext."` ( `item_id` INT(11) UNSIGNED NOT NULL DEFAULT '0', "; + + // add fields + + foreach ($this->config_filter_fields as $_filter_field => $_map_table_field ) { + list($_table, $_f) = explode(".", $_map_table_field); + + $_table_info = $this->db->getTableInfo($_table); + + $column_default = $_table_info[$_f]['COLUMN_DEFAULT']; + + $default_value = (in_array($_table_info[$_f]['DATA_TYPE'], ['mediumint', 'int', 'tinyint', 'double', 'float'])) ? 0 : $column_default; + + $sql .= " `".$_filter_field."` ".$_table_info[$_f]['COLUMN_TYPE']." NOT NULL DEFAULT '".$default_value."', "; + } + + foreach ($this->config_fulltext_fields as $_filter_field => $_t ) { + $sql .= " `".$_filter_field."` TEXT NULL , "; + + // create field-data to compare new values before re-indexing + $sql .= " `".$_filter_field."_base` TEXT NULL , "; + } + + $sql .= " `create_time` int(11) NOT NULL DEFAULT '0', "; + $sql .= " `last_update` int(11) NOT NULL DEFAULT '0', "; + + // index + + foreach ($this->config_filter_fields as $_filter_field => $_data_type ) { + $sql .= " INDEX (`".$_filter_field."`), "; + } + + // fulltext on separate columns + foreach ($this->config_fulltext_fields as $_filter_field => $_t ) { + $sql .= " FULLTEXT INDEX (`".$_filter_field."` ) , "; + } + + // create full text for all columns combined, so we can do query match(col_1, col_2) against keyword + $_filter_fields = array_keys($this->config_fulltext_fields); + if(sizeof($_filter_fields)) $sql .= " FULLTEXT INDEX (".join(", ", $_filter_fields).") , "; + + $sql .= " PRIMARY KEY ( `item_id` ) "; + $sql .= " ) ENGINE=InnoDB DEFAULT CHARSET=utf8"; + + //return $sql; + $this->db->runQuery($sql); + + // add foreign key constraint + $this->db->runQuery(" + ALTER TABLE `".$this->tb_fulltext."` ADD FOREIGN KEY (`item_id`) REFERENCES `".$this->tb_main."`(`id`) ON DELETE CASCADE; + "); + + return true; + } + + //11-11-2012 chuyen text thanh tieng viet voi dau chuyen doi: vd. nhà = nhas + //because mysql allow fulltext search with minimum 4 characters, we have a trick: add hura to everyone what has length < 4 + protected function make_text_search_vn($text, $enable_star = false){ + $text = Language::convertText($text); + $text = preg_replace("@[^a-z0-9\s]@si", " ", strtolower($text)); + //$text = str_replace(" "," ",$text); + $text_ele = array_filter(explode(" ", $text)); + $new_text = ""; + foreach($text_ele as $ele) { + if($ele == '/' ) continue; //skip this character if it stands alone + + if(strlen($ele) < 4 || $ele == 'plus') $new_text .= " hura".$ele; + else $new_text .= " ".$ele; + } + return trim($new_text); + } + + //11-11-2012 chuyen text thanh tieng viet khong dau: vd. nhà = nha + /* + @variable: + - $text: text to be index + + //16-11-2012 + - $enable_star: + true: allow search abcd by word: ab or abc + false: not enabled + - $min_len: if $enable_star = true, abcd -> tao thanh: ab, abd, abcd (if $min_len = 2) + */ + protected function make_text_clean($_text, $enable_star = false){ + //$text = Language::chuyenKhongdau(Language::convert_lower($text)); + $text = Language::chuyenKhongdau($_text); + + $text = preg_replace("@[^a-z0-9\s]@si", " ", strtolower($text)); + + $text_ele = array_filter(explode(" ", $text)); + $new_text = ""; + foreach($text_ele as $ele) { + + if($this->star_search && $enable_star) { + + $word_list = static::create_star_search($ele, $this->min_star_search_length); + foreach($word_list as $new_ele) { + if(strlen($new_ele) < 4 || $ele == 'plus') $new_text .= " hura".$new_ele; + else $new_text .= " ".$new_ele; + } + + }else{ + if(strlen($ele) < 4 || $ele == 'plus') $new_text .= " hura".$ele; + else $new_text .= " ".$ele; + } + } + return trim($new_text); + } + + //16-11-2012 + //create all possible combination for a word for star search: abcd -> tao thanh: ab, abd, abcd, bc, bcd, cd (if $min_len = 2) + protected function create_star_search($word, $min_len = 2){ + if($min_len < 2) $min_len = 2; //in case missing value for CONFIG_STAR_SEARCH_MIN_LENGTH + $word_len = strlen($word); + $result = array(); + if($word_len <= $min_len) return array($word); + for($i = $min_len; $i < $word_len; $i ++ ){ + $result[] = substr($word, 0, $i); + } + $result[] = $word; + + //09-01-2012 + //reduce $word 1 character to create new word and create combination from there + $new_word = substr($word, 1); + $new_result = self::create_star_search($new_word, $min_len = 2); + foreach($new_result as $el){ + $result[] = $el; + } + + return array_unique($result); + } + +} diff --git a/inc/Hura8/System/ModuleManager.php b/inc/Hura8/System/ModuleManager.php new file mode 100644 index 0000000..80c8a90 --- /dev/null +++ b/inc/Hura8/System/ModuleManager.php @@ -0,0 +1,39 @@ + $maxPage]); + } + + public static function pageSizeSelectBox($default_size = 15) { + $html = [""; + + return join("", $html); + } + + //allow to select pageSize + public static function getPageSizes() { + $sizes = [15, 30, 50]; + $final_list = []; + foreach ($sizes as $size) { + $final_list[] = [ + "size" => $size, + "name" => $size . "/ trang", + "url" => Url::buildUrl($_SERVER['REQUEST_URI'], ["page" => '', "pageSize" => $size]) + ]; + } + + return $final_list; + } + + //Function nhay trang paging + public static function pagingAjax($totalResults,$numPerRow,$currentURL,$maxPageDisplay,$holder_id){ + $currentPage = isset($_GET["page"]) ? (int) $_GET["page"] : 1; + $currentURL = str_replace(array("?page=".$currentPage, "&page=".$currentPage),"",$currentURL); + if(strpos($currentURL, "?") !== false) $currentURL .= "&"; + else $currentURL .= "?"; + + $maxPage = ceil($totalResults/$numPerRow); + //max page to see : 20 + $maxPage = ($maxPage > 50) ? 50 : $maxPage; + + if($maxPage==1) return ""; + $farLeft ="..."; + $farRight ="..."; + $link =""; + //$maxPageDisplay la so page se hien ra, thuong lay 7 + if($currentPage > 1) $link .= ""; + else $link .= ""; + $link .= ""; + + if($maxPage<=$maxPageDisplay){ + + for($i=1;$i<=$maxPage;$i++){ + if($i==$currentPage) $link .= ""; + else $link .= ""; + } + + if($currentPage < $maxPage) $link .= ""; + + }else{ + //Vay la tong so trang nhieu hon so trang muon hien ra + if($currentPage<=(ceil($maxPageDisplay/2))){ + + //Neu trang dang xem dang o muc 1,2,3,4 + for($i=1;$i<=$maxPageDisplay;$i++){ + if($i==$currentPage) $link .= ""; + else $link .= ""; + } + $link .=$farRight; + //$link .= "";//the last page + $link .= ""; + + }elseif($currentPage<=($maxPage-round($maxPageDisplay/2,0))){ + //Neu trang dang xem o muc cao hon 4 + $link .= "";//the 1st page + $link .=$farLeft; + for($i=($currentPage-round($maxPageDisplay/2,0)+1);$i<($currentPage+round($maxPageDisplay/2,0));$i++){ + if($i==$currentPage) $link .= ""; + else $link .= ""; + } + $link .=$farRight; + //$link .= "";//the last page + $link .= ""; + }else{ + //May trang cuoi cung + $link .= "";//the 1st page + $link .=$farLeft; + for($i=($maxPage-$maxPageDisplay+1);$i<=$maxPage;$i++){ + if($i==$currentPage) $link .= ""; + else $link .= ""; + } + } + } + $link .= "
<<Xem$i$i>>$i$i".$maxPage.">>1$i$i".$maxPage.">>1$i$i
"; + //Lua chon hien thi, phai it nhat tu 2 trang tro len moi hien ra + + if($maxPage>1){ + return $link; + } + + return false; + } + + // 04-Sept-2020 + public static function paging_template($totalResults, $numPerRow, $currentURL = '', $maxPageDisplay = 7){ + // parameter variables + if(!$currentURL) $currentURL = $_SERVER['REQUEST_URI']; + $url_parts = self::destructUrlWithCustomPageParam($currentURL); + /*[ + 'path' => $url_path, + 'query' => $query_params, + 'page' => $current_page, + ];*/ + + $query_without_page = $url_parts['query']; + $current_page = (isset($url_parts['page']) && $url_parts['page']) ? intval($url_parts['page']) : 1; + $total_pages = ceil($totalResults / $numPerRow); + $page_space = ceil(($maxPageDisplay - 1) / 2); + if($current_page > $total_pages) $current_page = $total_pages; + + /* + * $current_page is expected to be always in the middle: + * 3 = $page_space = ( $maxPageDisplay = 7-1/2) + * if current_page = 4 -> expect start = 4-3 = 1, expect end = 4+3 = 7 + * if current_page = 2 -> expect start = 2-3 = -1, expect end = 2+3 = 5. Since -1 is not valid, we need to add 2 to make it at least 1, so the end need to add 2 as well, so the result is: start = 1, end = 7 + * if current_page = 6 -> expect start = 6-3 = 3, expect end = 6+3 = 9. if max page =8, the end need to offset 9-8 = 1, so the start also need to offset 1, the result: start=3-1 = 2, end=9-1=8 + * */ + + // expect result + $start_index = $current_page - $page_space; + $end_index = $current_page + $page_space; + + //calculate offset + if($start_index < 1) { + $offset = 1 - $start_index; + $start_index = 1; + $end_index += $offset; + if($end_index > $total_pages) $end_index = $total_pages; + }else if($end_index > $total_pages) { + $offset = $end_index - $total_pages; + $end_index = $total_pages; + $start_index -= $offset; + if($start_index < 1) $start_index = 1; + } + + $page_collection = []; + $build_table = []; + + // add prev + if($start_index > 1) { + $query_without_page['page'] = $current_page-1; + $url = self::buildUrlWithCustomPageParam($url_parts['path'], $query_without_page); + + $build_table[] = " << "; + $page_collection[] = [ + "name" => "prev", + "url" => $url, + "is_active" => 0, + ]; + } + + for($i = $start_index; $i <= $end_index ; $i++) { + $query_without_page['page'] = $i; + $url = self::buildUrlWithCustomPageParam($url_parts['path'], $query_without_page); + $is_active = ($i == $current_page) ? 1 : 0; + + $page_collection[] = [ + "name" => $i, + "url" => $url, + "is_active" => $is_active, + ]; + + if($is_active) { + $build_table[] = "".$i.""; + }else{ + $build_table[] = "".$i.""; + } + } + + // add forward + if($end_index < $total_pages) { + $query_without_page['page'] = $current_page + 1; + $url = self::buildUrlWithCustomPageParam($url_parts['path'], $query_without_page); + $build_table[] = " >> "; + $page_collection[] = [ + "name" => "next", + "url" => $url, + "is_active" => 0, + ]; + } + + //------------------- + if($total_pages > 1 ){ + $tb_page = join("", [ + '', + join('', $build_table) , + "
" + ]) ; + + return array($page_collection, $tb_page, $total_pages); + } + + return array([], '', 1); + } + + // Url with page parameter format + // default: domain/path?key1=value1&key2=value2&page=1|2|3... + // custom1: domain/path/1|2|3/?key1=value1&key2=value2 + public static function buildUrlWithCustomPageParam($url_path, array $params){ + $url_format_type = self::getUrlWithCustomPageFormat(); + + $params_without_page = $params; + $page_number = 1; + if(isset($params['page'])) { + $page_number = intval($params['page']); + unset($params_without_page['page']); + } + + // custom1 (use case: hanoicomputer.vn) + if($url_format_type == 'custom1') { + if($page_number > 1){ + $url_path = $url_path."/".$page_number."/"; + } + }else{ + // default + if($page_number > 1){ + // push page to the end, always + $params_without_page['page'] = $page_number; + } + } + + + if(sizeof($params_without_page)) { + return join("", [$url_path, "?", http_build_query($params_without_page)]); + } + + return $url_path; + } + + // destruct Url to get url_path and query params + public static function destructUrlWithCustomPageParam($full_url){ + $cache_key = md5($full_url); + $cached_value = self::getCache($cache_key); + // if in cache, get right away + if($cached_value) return $cached_value; + + // not in cache, build and set cache + $url_format_type = self::getUrlWithCustomPageFormat(); + + // destruct + $parsed_url = explode("?", $full_url); + $url_path = isset($parsed_url[0]) ? $parsed_url[0] : ''; + $query_part = isset($parsed_url[1]) ? $parsed_url[1] : ''; + $query_params = []; + $current_page = 1; + + if($query_part) { + parse_str($query_part, $query_params); + } + + // custom1: url format= domain/path/1|2|3/?key1=value1&key2=value2 + if($url_format_type == 'custom1') { + $match = []; + if(preg_match("{/(.*?)/([0-9]+)/$}", $url_path, $match)){ + if(isset($match[2]) && $match[2] > 1) $current_page = $match[2]; + $url_path = "/".$match[1]; + } + + } else { + // default + if(isset($query_params['page'])) { + $current_page = $query_params['page']; + unset($query_params['page']); + } + } + + $cached_value = [ + 'path' => $url_path, + 'query' => $query_params, + 'page' => $current_page, + ]; + + self::setCache($cache_key, $cached_value); + + return $cached_value; + } + + + // check if user in in the admin panel + protected static function is_in_admin(){ + return substr($_SERVER['REQUEST_URI'], 0, 7) == '/admin/'; + } + + // Url with page parameter format + // default: domain/path?key1=value1&key2=value2&page=1|2|3... + // custom1: domain/path/1|2|3/?key1=value1&key2=value2 + protected static function getUrlWithCustomPageFormat() { + $url_format_type = "default"; + + // keep default for admin panel + if(self::is_in_admin()) { + return $url_format_type; + } + + // or search page + if(substr($_SERVER['REQUEST_URI'], 0, 4) == '/tim') { + return $url_format_type; + } + + if(defined("URL_PAGING_FORMAT") && URL_PAGING_FORMAT) { + $url_format_type = URL_PAGING_FORMAT; + } + + return $url_format_type; + } + + protected static function getCache($key) { + return isset(self::$cache[$key]) ? unserialize(self::$cache[$key]) : null; + } + + protected static function setCache($key, $value) { + self::$cache[$key] = serialize($value); + } + + //Function nhay trang + public static function paging($totalResults, $numPerRow, $currentURL, $maxPageDisplay, $limitPage = 50){ + $currentPage = isset($_GET["page"]) ? (int) $_GET["page"] : 1; + $currentURL = str_replace(array("?page=".$currentPage, "&page=".$currentPage),"",$currentURL); + //build_url($currentURL, array("page"=>'')); + + $first_url = $currentURL; //when $page=1 + + if(strpos($currentURL, "?") !== false) $currentURL .= "&"; + else $currentURL .= "?"; + + $maxPage = ceil($totalResults/$numPerRow); + //max page to see : 20 + if($limitPage > 0){ + $maxPage = ($maxPage > $limitPage) ? $limitPage : $maxPage; + } + + if($maxPage==1) return ""; + $farLeft ="..."; + $farRight ="..."; + $link =""; + //$maxPageDisplay la so page se hien ra, thuong lay 7 + if($currentPage == 2) $link .= ""; + else if($currentPage > 2) $link .= ""; + else $link .= ""; + + $link .= ""; + + if($maxPage<=$maxPageDisplay){ + + for($i=1;$i<=$maxPage;$i++){ + if($i==$currentPage) $link .= ""; + else { + if($i== 1) $link .= ""; + else $link .= ""; + } + } + + if($currentPage < $maxPage) $link .= ""; + + }else{ + + //Vay la tong so trang nhieu hon so trang muon hien ra + if($currentPage<=(ceil($maxPageDisplay/2))){ + + //Neu trang dang xem dang o muc 1,2,3,4 + for($i=1;$i<=$maxPageDisplay;$i++){ + if($i==$currentPage) $link .= ""; + else { + if($i== 1) $link .= ""; + else $link .= ""; + } + } + $link .=$farRight; + //$link .= "";//the last page + $link .= ""; + + }elseif($currentPage<=($maxPage-round($maxPageDisplay/2,0))){ + //Neu trang dang xem o muc cao hon 4 + $link .= "";//the 1st page + $link .= $farLeft; + for($i=($currentPage-round($maxPageDisplay/2,0)+1);$i<($currentPage+round($maxPageDisplay/2,0));$i++){ + if($i==$currentPage) $link .= ""; + else $link .= ""; + } + $link .= $farRight; + //$link .= "";//the last page + $link .= ""; + }else{ + //May trang cuoi cung + $link .= "";//the 1st page + $link .= $farLeft; + for($i=($maxPage-$maxPageDisplay+1);$i<=$maxPage;$i++){ + if($i==$currentPage) $link .= ""; + else $link .= ""; + } + } + } + + $link .= "
Quay lạiQuay lạiXem trang$i$i$iTiếp theo$i$i$i".$maxPage."Tiếp theo1$i$i".$maxPage."Tiếp theo1$i$i
"; + + //Lua chon hien thi, phai it nhat tu 2 trang tro len moi hien ra + if($maxPage > 1){ + return $link; + } + + return false; + } +} diff --git a/inc/Hura8/System/Permission.php b/inc/Hura8/System/Permission.php new file mode 100644 index 0000000..4e8ccbb --- /dev/null +++ b/inc/Hura8/System/Permission.php @@ -0,0 +1,39 @@ + $is_enabled) { + if($is_enabled && isset($plugin_mapping[$key]) && $plugin_mapping[$key]) { + $file = "display_plugin/". $plugin_mapping[$key]; + if(file_exists($file)) { + $page_plugin_data[$key] = include $file; + } + } + } + + return $page_plugin_data; + + } else { + + die($plugin_mapping_file." not exist"); + + } + } + + + protected static function getPlugins() { + + $config_file = ROOT_DIR . "/config/client/config_plugin.php"; + + if(file_exists($config_file)) { + + $config_plugins = include $config_file; + //global plugin should over-ride local one + $allowed_plugins = $config_plugins; + + foreach ($config_plugins as $module => $content) { + if($module == "global") continue; //skip + + foreach ($content as $option => $option_pages) { + foreach ($option_pages as $page => $precate) { + if($precate && array_key_exists($page, $allowed_plugins['global']) && $allowed_plugins['global'][$page] ) { + $allowed_plugins[$module][$option][$page] = false; + } + } + } + } + + return $allowed_plugins; + + } else { + + die($config_file." not exist"); + + } + } + +} diff --git a/inc/Hura8/System/RainTPL.php b/inc/Hura8/System/RainTPL.php new file mode 100644 index 0000000..18c5231 --- /dev/null +++ b/inc/Hura8/System/RainTPL.php @@ -0,0 +1,1023 @@ +), stylesheet (), script ( + + + + \ No newline at end of file diff --git a/old/template/article/home.html b/old/template/article/home.html new file mode 100644 index 0000000..69baec0 --- /dev/null +++ b/old/template/article/home.html @@ -0,0 +1,137 @@ +

+
+

Bài viết

+ +
+ + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Sản phẩm (Tổng số: 1.742) Thống kê Quản trị Lựa chọn
1 + + + [#2481] + Elgato – Happy New Year 2024 Deal Hot Đầu Xuân + +
Thời gian chương trình : 15.02.2024 – 24.02.2024
+ +
+ STT + +
+
+

- Xem: 90

+

- Thích: 0

+

- Comment: 0

+
+

Cập nhật: Nguyễn MInh Hải

+

(15-02-2024, 2:57 pm)

+
+
+ + + + + + + + + + + + + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+ +
+
\ No newline at end of file diff --git a/old/template/article/relation.html b/old/template/article/relation.html new file mode 100644 index 0000000..5edc680 --- /dev/null +++ b/old/template/article/relation.html @@ -0,0 +1,124 @@ +{% assign _check_type = global.url | split: 'related_item_type=' %} + + + +
+
+ +
+

Sửa nội dung

+ +

ROG FALCHION LOW PROFILE – CHƠI GAME TRÊN MAC LÀ + CHUYỆN NHỎ

+ + +
+ + + + + +
+ {% if _check_type[1] == 'product'%} + + {% include 'article/relation_product' %} + + {% elsif _check_type[1] == 'product-category' %} + + {% include 'article/relation_product_category' %} + + {% endif %} +
+
+
+ + + + \ No newline at end of file diff --git a/old/template/article/relation_add.html b/old/template/article/relation_add.html new file mode 100644 index 0000000..9ad9cbd --- /dev/null +++ b/old/template/article/relation_add.html @@ -0,0 +1,70 @@ +{% assign _check_type = global.url | split: 'related_item_type=' %} + + + +
+
+
+

Thêm liên quan

+ +

+ Thêm liên quan: HuraSoft - Test tin tức +

+
+ + + + {% if _check_type[1] == 'product' %} + + {% include 'article/relation_product_add' %} + + {% elsif _check_type[1] == 'product-category' %} + + {% include 'article/relation_product_category_add' %} + + {% endif %} + +
+
+ + + + + + + \ No newline at end of file diff --git a/old/template/article/relation_product.html b/old/template/article/relation_product.html new file mode 100644 index 0000000..e85ba4b --- /dev/null +++ b/old/template/article/relation_product.html @@ -0,0 +1,68 @@ +
+

Danh sách liên quan: HuraSoft - Test tin tức

+ + + + + Thêm liên quan mới + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Tên Thông tin bán hàng Cài đặt Lựa chọn
1 + CPU Intel Pentium Gold G6405 (Intel LGA1200 - 2 Core - 4 Thread - Base 4.1Ghz - Cache 4MB + +

- Mã kho :  CPUI0132

+

- Lượt xem:  21577

+

- Giá bán :  1.999.000 vnđ

+

- Kho hàng : 3

+

- Bảo hành : 36 Tháng

+
+

STT

+ +
+ +
+
+ +
+ + 1 + 2 + 3 + 4 + +
\ No newline at end of file diff --git a/old/template/article/relation_product_add.html b/old/template/article/relation_product_add.html new file mode 100644 index 0000000..34183ca --- /dev/null +++ b/old/template/article/relation_product_add.html @@ -0,0 +1,78 @@ +
+

Chọn sản phẩm liên quan - Tổng số: 28872

+ +
+ + +

Chọn danh mục

+ + + + + + +
+ + + Chọn toàn bộ danh sách + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Tên Thông tin bán hàng Lựa chọn
1 + + + CPU Intel Pentium Gold G6405 Intel LGA1200 - 2 Core - 4 Thread - Base 4.1Ghz - Cache 4MB +

Cập nhật: Hôm nay, 11:11 am

+
+ - Mã kho : CPUI0132
+ - Lượt xem: 21577
+ - Giá bán : 1.999.000 vnđ
+ - Kho hàng : 3
+ - Bảo hành : 36 Tháng +
+ + Chọn liên quan + +
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
\ No newline at end of file diff --git a/old/template/article/relation_product_category.html b/old/template/article/relation_product_category.html new file mode 100644 index 0000000..d38eeea --- /dev/null +++ b/old/template/article/relation_product_category.html @@ -0,0 +1,53 @@ +
+

Danh sách liên quan: HuraSoft - Test tin tức

+ + + + + Thêm liên quan mới + +
+ +
+ + + + + + + + + + + + + + + + + + + +
STT Tên Lựa chọn
1 +

Laptop - Tablet - Mobile

+ +

STT

+ +
+ + Bỏ lựa chọn + +
+
+ +
+ + 1 + 2 + 3 + 4 + +
\ No newline at end of file diff --git a/old/template/article/relation_product_category_add.html b/old/template/article/relation_product_category_add.html new file mode 100644 index 0000000..2102cf2 --- /dev/null +++ b/old/template/article/relation_product_category_add.html @@ -0,0 +1,60 @@ +
+

Chọn danh mục liên quan

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Danh mục Lựa chọn
1 Danh mục cấp 1 + + Chọn liên quan + + +

Đã chọn

+
1.1 Danh mục cấp 2 + + Chọn liên quan + +
1.1.1 Danh mục cấp 3 + + Chọn liên quan + +
1.1.1.1 Danh mục cấp 4 + + Chọn liên quan + +
+
+
\ No newline at end of file diff --git a/old/template/article/tag.html b/old/template/article/tag.html new file mode 100644 index 0000000..84bcdf1 --- /dev/null +++ b/old/template/article/tag.html @@ -0,0 +1,123 @@ + + +
+
+ +
+

Sửa nội dung

+ +

ROG FALCHION LOW PROFILE – CHƠI GAME TRÊN MAC LÀ CHUYỆN NHỎ

+ + +
+ + +
+ Chỉnh sửa + + {% if global.url contains 'id=' %} + Tag + Liên quan: Sản phẩm + Liên quan: Danh mục sản phẩm + {% endif %} +
+ + +
+ + + Chọn tag mới + + +
+ + + + + + + + + + + + + + + + + +
STT Tag Lựa chọn
1 Laptop GTX 1050  + +
+
+ +
+ + +
+
+
+ + + + \ No newline at end of file diff --git a/old/template/banner/home.html b/old/template/banner/home.html new file mode 100644 index 0000000..ed78ecb --- /dev/null +++ b/old/template/banner/home.html @@ -0,0 +1,200 @@ +
+
+

Danh sách banner

+ +
+ + + + + + +
+ +
+
+ + + + + Chọn + danh mục sản phẩm để xem +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Thông tin Thứ tự Click Chỉnh sửa
1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tên gọi:
Link tới:
File:/media/banner/21_06-1f22593caab42ff1e4ebf38094a75afe.jpeg
Kích thước:Rộng x Cao (Width x Height) = 3130px x 2075px
Link:/ad.php?id=236
Thời gian hiển thị:Luôn hiển thị, chỉ ẩn khi hạ bằng tay
Cập nhật:21-06-2023, 11:08 am
+
0 0 +
+ + + + + + + + +
+
2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tên gọi:
Link tới:
File:/media/banner/21_06-1f22593caab42ff1e4ebf38094a75afe.jpeg
Kích thước:Rộng x Cao (Width x Height)=3130px x 2075px
Link:/ad.php?id=236
Thời gian hiển thị:Luôn hiển thị, chỉ ẩn khi hạ bằng tay
Cập nhật:21-06-2023, 11:08 am
+
0 0 +
+ + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+ +
+
\ No newline at end of file diff --git a/old/template/banner/location.html b/old/template/banner/location.html new file mode 100644 index 0000000..aaed2b5 --- /dev/null +++ b/old/template/banner/location.html @@ -0,0 +1,67 @@ + + +
+

Quản lý vị trí hiển thị banner

+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STT ID Template File Vị trí Ngày tạo Chỉnh sửa
1 +

ID: 43

+

banner_webmau4_mb

+
homepage Banner MB Webmau4 30-11-2024, 12:00 am +
+ Xem + banner + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
\ No newline at end of file diff --git a/old/template/banner/location_add.html b/old/template/banner/location_add.html new file mode 100644 index 0000000..bfe1612 --- /dev/null +++ b/old/template/banner/location_add.html @@ -0,0 +1,123 @@ + + +
+
+
+

Cập nhật lại vị trí banner

+ + + + + Thêm vị trí mới + + +

+ banner_under_slider_mobile_webmau3 +

+
+ + + +
+

Thông tin cơ bản

+ + +
+
+ Chọn template hiển thị + +
+ + + +
+
+ + + +
+ + +
+
+ Mã vị trí (*) * chỉ gồm ký tự a-z0-9 và dấu _ + +
+ + + +
+
+ + +
+ + +
+
+ Đặt tên vị trí (*) + +
+ + + +
+
+ + +
+ + +
+

Mô tả tóm tắt

+ + +
+ +
+ + +
+
+
\ No newline at end of file diff --git a/old/template/banner/upload.html b/old/template/banner/upload.html new file mode 100644 index 0000000..cc907a2 --- /dev/null +++ b/old/template/banner/upload.html @@ -0,0 +1,417 @@ + + +
+ + +
+ {% if global.url contains 'id=' %} + {% endif %} +

+ Cập nhật lại banner

+
+ + +
+ + + + + + + + + + + +
+

File

+
+

+ /media/banner/21_06-1f22593caab42ff1e4ebf38094a75afe.jpeg

+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Link theo dõi /ad.php?id=236
Đặt tên banner để theo dõi + +
Mô tả + +
Kích thước file + Chiều dài (width) : + px - + Chiều cao (height) + px + (* bắt buộc cho file flash) +
File banner +
+ Upload từ máy tính + +
+ +
+

Hoặc điền địa chỉ đầy đủ trên mạng (v.d. + http://web-acb.com/anh.jpg):

+ + +
+
Địa chỉ URL đích + +
Thứ tự hiển thị + + (nếu nhiều banner cùng 1 vị trí, số cao xếp trước) +
Thời gian hiển thị + + + + +
+

Bắt đầu

+
+ + +
+ +

Giờ

+ + +
+ +
+

Kết thúc

+
+ + +
+ +

Giờ

+ + +
+
Hiển thị + + + +
Khác + +
+
+ + +
+
+ +
+
+ + + \ No newline at end of file diff --git a/old/template/customer/comment.html b/old/template/customer/comment.html new file mode 100644 index 0000000..5e22399 --- /dev/null +++ b/old/template/customer/comment.html @@ -0,0 +1,211 @@ +
+
+

Tổng hợp trao đổi

+ + + +
+ + +
+ + +
+

Tổng số: 1.996

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
STTPhản hồiThời gianTác giảSản phẩmNội dungLựa chọn
1109-08-2023, 11:24 am +
+ Nguyễn An +

Quản + trị + viên

+
+

annv@hurasoft.com

+
+ Màn Hình AOC 24B1XHS 23.8 inch Full HD + + test reply + + + +
+
+ + + + + + + +
+
\ No newline at end of file diff --git a/old/template/customer/customer_add.html b/old/template/customer/customer_add.html new file mode 100644 index 0000000..717053e --- /dev/null +++ b/old/template/customer/customer_add.html @@ -0,0 +1,175 @@ +
+ + +
+
+
+
+

Cập nhật lại khách hàng

+ Khách hàng: Nguyễn Duy Thành +
+ + + + Thêm khách hàng mới + +
+
+ +
+
+ +
+
+
+ Thông tin khách hàng +
+ Thuộc tính khách hàng + Quản lý nhóm khách hàng +
+ +
+ + + +
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+ +
+ + + + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/old/template/customer/customer_contact.html b/old/template/customer/customer_contact.html new file mode 100644 index 0000000..b3c4b9b --- /dev/null +++ b/old/template/customer/customer_contact.html @@ -0,0 +1,73 @@ +
+
+

Khách hàng liên hệ

+ +
+ + +
+ + +
+

Tổng hợp các liên hệ của khách hàng từ form liên hệ trên trang liên + hệ của website

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STTThời gianKhách hàngThông tinNội dung liên hệGhi chúTrạng thái
109-08-2023, 11:24 amĐức Anh +

- Email: ducanhnguyen199x@gmail.com

+

- Tel: 0976642092

+

- Mobile: 0976642092

+
+ Trả góp sản phẩm: HHWS AMD RYZEN 9 3900XT | 16G | NVIDIA RTX + 2060 6G +

- Tên: no name

+

- SĐT: 0147258369

+

- Nội dung: fsfdf

+
+ + Cập + nhật + + +
+ + +
+
+
+
+
\ No newline at end of file diff --git a/old/template/customer/customer_group.html b/old/template/customer/customer_group.html new file mode 100644 index 0000000..fa25270 --- /dev/null +++ b/old/template/customer/customer_group.html @@ -0,0 +1,56 @@ +
+
+

Nhóm khách hàng

+ + + + + + + + + + + + + + + + + + + + + + +
STTMã nhómTên gọi/miêu tảKhách hàngChỉnh sửa
10Khách vip + Xem + danh sách + +
+ + + + + + + +
+
+ +
+
\ No newline at end of file diff --git a/old/template/customer/customer_group_add.html b/old/template/customer/customer_group_add.html new file mode 100644 index 0000000..eb8fe27 --- /dev/null +++ b/old/template/customer/customer_group_add.html @@ -0,0 +1,75 @@ +
+ + + +
+
+
+
+

Cập nhật nhóm khách hàng

+
+ + + + Thêm nhóm khách hàng + +
+

Hướng dẫn: Nhóm khách hàng bao gồm các khách hàng cùng chia sẻ một số đặc + điểm + nhất định, giúp cho việc + quản lý và chăm + sóc khách hàng tốt hơn. Ví dụ các nhóm khách hàng thường được tạo là: Khách hàng thân thiết, Khách hàng + nữ, Khách hàng + VIP, Khách văn phòng v..v. +

+
+ +
+
+ +
+
+
+ +
+ + +
+
+ + (* Dùng phân biệt từng nhóm, Mã chỉ bao gồm các ký tự + a-z0-9 và _, + không dấu và không có + dấu cách. Ví dụ: vip1, dai_ly + ...) + +
+
+ + +
+ +
+
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/old/template/customer/customer_group_info.html b/old/template/customer/customer_group_info.html new file mode 100644 index 0000000..d40537b --- /dev/null +++ b/old/template/customer/customer_group_info.html @@ -0,0 +1,75 @@ +
+ + + +
+
+

Thông tin khách hàng liên hệ

+

Lê Văn Ninh

+
+ +
+
+ +
+
+
+
+ Thông tin liên hệ +
+ Họ tên: +

Lê Văn Ninh

+
+
+ Địa chỉ email: +

no_email_1708170779@mail.com

+
+
+ Số điện thoại: +

0356723619

+
+
+ Thời gian liên hệ: +

17-02-2024, 6:56 pm

+
+
+ +
+ Nội dung chi tiết +

+ Tìm sản phẩm cần định giá: + - Mẫu máy cần thu: intel i3-4160 CPU @ 3.600Hz 3.60 Ghz + - Địa chỉ: Phường Quảng Cư ,Tp.Sầm Sơn + - Mô tả ngắn về ngoại hình và tình trạng thiết bị: đang dùng tốt + - Sản phẩm bạn cần tư vấn lên đời: CPU INTEL CORE + I3-12100F (3.3GHZ TURBO UP TO 4.3GHZ, 4 NHÂN 8 LUỒNG, 12MB CACHE, 58W)- SOCKET INTEL LGA + 1700) +

+
+ +
+ + +
+ +
+
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/old/template/customer/customer_newsletter.html b/old/template/customer/customer_newsletter.html new file mode 100644 index 0000000..e286ce9 --- /dev/null +++ b/old/template/customer/customer_newsletter.html @@ -0,0 +1,58 @@ +
+
+

Khách hàng góp ý

+ + + +
+ + + + + + + + + + + + + + + + + + + + + +
STTHọ tênEmailNgày đăng kýThông tin khácLựa chọn
1Trung Hiếutrunghieu27072009@gmail.com19-07-2023, 4:45 pmtest reply +
+ + +
+
+
+
+
\ No newline at end of file diff --git a/old/template/customer/customer_review.html b/old/template/customer/customer_review.html new file mode 100644 index 0000000..702c064 --- /dev/null +++ b/old/template/customer/customer_review.html @@ -0,0 +1,80 @@ +
+
+

Khách hàng góp ý

+
+

Tổng hợp các liên hệ của khách hàng từ form liên hệ trên trang liên hệ của website

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTKhách hàngNội dungLựa chọn
3 +

huyth.c2hoangdieu.haugiang@gmail.com

+

Trịnh Hoàng Huy

+

29-01-2018, 1:34 am

+
+ Góp ý (rate:0) +

cửa hàng nên có thêm các dòng SSD Samsung 960 PRO và Samsung 960 EVO để có thêm nhiều lựa + chọn

+
+ + + +
3 +

huyth.c2hoangdieu.haugiang@gmail.com

+

Trịnh Hoàng Huy

+

29-01-2018, 1:34 am

+
+ Góp ý (rate:0) +

cửa hàng nên có thêm các dòng SSD Samsung 960 PRO và Samsung 960 EVO để có thêm nhiều lựa + chọn

+
+ + + +
3 +

huyth.c2hoangdieu.haugiang@gmail.com

+

Trịnh Hoàng Huy

+

29-01-2018, 1:34 am

+
+ Góp ý (rate:0) +

cửa hàng nên có thêm các dòng SSD Samsung 960 PRO và Samsung 960 EVO để có thêm nhiều lựa + chọn

+
+ + + +
+
+
+
\ No newline at end of file diff --git a/old/template/customer/home.html b/old/template/customer/home.html new file mode 100644 index 0000000..4f54f63 --- /dev/null +++ b/old/template/customer/home.html @@ -0,0 +1,130 @@ +
+
+

Danh sách khách hàng

+ + + +
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTIDKhách hàngThông tinTỉnh/TPTrạng tháiSố đơn hàngTổng giá trịThời gianChỉnh sửa
11767Đức Anh +

- Email: ducanhnguyen199x@gmail.com

+

- Tel: 0976642092

+

- Mobile: 0976642092

+
+ Hà Nội + +
Hoạt động
+
+ 0Thành công + + 0Thu + +

+ - Tham gia: 06-10-2023, 10:46 am +

+

+ - Đăng nhập cuối: Chưa bao giờ +

+
+ + +
+
+ +
+ + + + 1 + 2 + 3 + 4 + 5 + + + +
+ +
+
\ No newline at end of file diff --git a/old/template/customer/review.html b/old/template/customer/review.html new file mode 100644 index 0000000..183fa98 --- /dev/null +++ b/old/template/customer/review.html @@ -0,0 +1,211 @@ +
+
+

Tổng hợp đánh giá

+ + + +
+ + +
+ + +
+

Tổng số: 1.996

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
STTPhản hồiThời gianTác giảSản phẩmNội dungLựa chọn
1109-08-2023, 11:24 am +
+ Nguyễn An +

Quản + trị + viên

+
+

annv@hurasoft.com

+
+ Màn Hình AOC 24B1XHS 23.8 inch Full HD + + test reply + + + +
+
+ + + + + + + +
+
\ No newline at end of file diff --git a/old/template/deal/deal_add.html b/old/template/deal/deal_add.html new file mode 100644 index 0000000..1743e8d --- /dev/null +++ b/old/template/deal/deal_add.html @@ -0,0 +1,384 @@ +{% if global.url contains '?id=' %} + +{% include 'deal/deal_detail' %} + +{% else %} + + + +
+

Chọn sản phẩm Deal

+ +
+ + +
+ +
+ + +
+ + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
STT Sản phẩm (Tổng số: 1.742) Thông tin khác Lựa chọn
1 + Màn hình Viewsonic VA3209U-4K (32 inch/UHD/IPS/60Hz/4ms) +

Mã kho: MOVI213

+

Danh mục: Màn Hình Máy Tính, Màn Hình Theo Hãng, Màn Hình ViewSonic

+

Bảo hành: 36 Tháng

+

Khuyến mại: + "ƯU ĐÃI Giảm ngay 10% vào thẻ nhớ khi khách hàng mua kèm Camera"

+
+

Giá: 10.599.000

+

Kho hàng: 0

+

Bảo hành: 36 Tháng

+
+ + Chọn sản phẩm + +
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
+ + + + + + + + + + + + + + +{% endif %} \ No newline at end of file diff --git a/old/template/deal/deal_detail.html b/old/template/deal/deal_detail.html new file mode 100644 index 0000000..decc2f3 --- /dev/null +++ b/old/template/deal/deal_detail.html @@ -0,0 +1,294 @@ + + +
+
+ +
+

Cập nhật deal

+ + + +

+ Màn hình máy tính HKC M20A6 20 Inch Full HD +

+
+ + +
+ +
+ + +
+ +

Thông tin deal

+ +
+

Tiêu đề của Deal

+ + +
+ +
+

Sản phẩm liên quan

+ +

+ Màn hình máy tính HKC M20A6 20 Inch Full HD + Xem link +

+
+ +
+

Mô tả tóm tắt về deal nếu có

+ + +
+ +

+ +

Thông tin bán Sản phẩm

+ +
+

Giá sản phẩm

+ +

17.999.000 VNĐ

+
+ +
+

Giá deal

+ + +
+ +
+
+

+ Số lượng bán + (Số đang có: 8) +

+ + +
+ +
+
+ Số lượng tối thiểu phải mua + +
+ + + +
+
+ + +
+ +
+
+ Số lượng tối đa được mua + +
+ + + +
+
+ + +
+
+ +
+

Thời gian hiệu lực *

+ +
+

Bắt đầu

+
+ + +
+ +

Giờ

+ + +
+ +
+

Kết thúc

+
+ + +
+ +

Giờ

+ + +
+
+ +
+

Thứ tự hiển thị

+ + +
+ + +
+

Hiển thị

+ + + + +
+ +

+ + +
+
+
\ No newline at end of file diff --git a/old/template/deal/home.html b/old/template/deal/home.html new file mode 100644 index 0000000..f4a0bde --- /dev/null +++ b/old/template/deal/home.html @@ -0,0 +1,178 @@ +
+
+

Danh sách Deal

+ +
+ + + + + + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + STT Ảnh Sản phẩm (Tổng số: 1.742) Thông tin Deal Thời gian Deal + + Công cụ
+ + 1 +
+ +
+
+ HH WORKSTATION CORE i9 10900K | 32GB | NVIDIA RTX 2060 6GB +

[SKU: LTLE950]

+

Số lượng: 60

+

Số tối thiểu cho 1 đơn hàng: 1

+

Tạo lúc: 25-10-2022, 4:56 pm

+

Cập nhật: 17-01-2024, 9:36 am

+
+

Giá Deal: + 35.000.000 vnđ + (Giảm: 4% ) +

+

Giá thị trường: 36.000.000 vnd

+

Số đơn hàng : 6

+

Số lượng đặt mua: 10

+

Lượt xem: 0

+
+ 01-01-2024, 1:00 pm đến + 31-03-2024, 8:00 pm + + + + + + + +
+ + + + + + + + + + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
+
+ + + + \ No newline at end of file diff --git a/old/template/deal/upload_deal_excel.html b/old/template/deal/upload_deal_excel.html new file mode 100644 index 0000000..9088bde --- /dev/null +++ b/old/template/deal/upload_deal_excel.html @@ -0,0 +1,127 @@ + + +
+
+ +
+

Thêm deal qua excel

+ +

Hướng dẫn: File sẽ thêm Sản phẩm nếu chưa có, và cập nhật Sản phẩm nếu có rồi

+ + + + Tải danh sách deal + +
+ + + + + +
+ +
+ Chú ý: Yêu cầu file excel theo đúng file mẫu. + + + + Tải file mẫu + +
+ + + + + + + + + + + + + +
File cập nhật + +
Cấu hình cột Excel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Cột ExcelNội dung
AMã kho (SKU)
BTên của deal
CGiá deal (dạng số)
DSố lượng mua tối thiểu (dạng số - mặc định là 1)
ESố lượng mua tối đa (dạng số - mặc định là 1)
FSố lượng (dạng số)
GNgày bắt đầu (DD/MM/YYYY)
HGiờ bắt đầu (hh:mm)
INgày kết thúc (DD/MM/YYYY)
JGiờ kết thúc (hh:mm)
KChọn hiển thị (yes|no)
LThứ tự hiển thị
+ +
+ +
+ + +
+
+
\ No newline at end of file diff --git a/old/template/distributor/form.html b/old/template/distributor/form.html new file mode 100644 index 0000000..c6ff084 --- /dev/null +++ b/old/template/distributor/form.html @@ -0,0 +1,170 @@ +
+ + +
+
+
+

Form đại lý

+ +
+

Nhà Thuốc Hiệp Phát

+
+
+
+ +
+
+
+ Cập nhật lại + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tên gọi + +
Mô tả + +
Tọa độ bản đồ +
+
+

Kinh độ

+ +
+ +
+

Vĩ độ

+ +
+
+
Tỉnh/TP + +
Quận huyện + +
Địa chỉ + +
Điện thoại + +
Website + +
Người liên hệ + +
Email liên hệ + +
STT + +
STT + +
Trạng thái +
+ + + +
+
+ +
+ +
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/old/template/distributor/home.html b/old/template/distributor/home.html new file mode 100644 index 0000000..86969f0 --- /dev/null +++ b/old/template/distributor/home.html @@ -0,0 +1,106 @@ +
+
+

Danh sách đại lý

+ +
+ + +
+ +
+
+

Lựa chọn ngôn ngữ

+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTĐại lýNGười liên hệĐịa chỉTỉnhĐiện thoạiLựa chọn
1Nhà Thuốc Hiệp PhátNguyễn Thị HiệpThửa đất số 126, Tờ BĐS 53, KP Phú Lạc, P.Hòa Hiệp Nam, TX Đông Hòa, PYPhú yên0982483346 + +
1Nhà Thuốc Hiệp PhátNguyễn Thị HiệpThửa đất số 126, Tờ BĐS 53, KP Phú Lạc, P.Hòa Hiệp Nam, TX Đông Hòa, PYPhú yên0982483346 + +
+ + +
+
\ No newline at end of file diff --git a/old/template/home/components/main_stat.html b/old/template/home/components/main_stat.html new file mode 100644 index 0000000..ad71f35 --- /dev/null +++ b/old/template/home/components/main_stat.html @@ -0,0 +1,210 @@ +
+
+
+

+ Đơn + hàng + mới

+ + + + +
+ +

10.000.000

+
+ +
+
+ + 10% +
+ +

Hôm qua

+
+
+ +
+
+
+

Khách + hàng + liên hệ qua website

+ + + + +
+ +

10.000.000

+
+ +
+
+ + 10% +
+ +

Hôm qua

+
+
+ +
+
+
+

Đơn + trả + góp

+ + + + +
+ +

10.000.000

+
+ +
+
+ + 10% +
+ +

Hôm qua

+
+
+ +
+
+
+

Đơn + hoàn + trả

+ + + + +
+ +

10.000.000

+
+ +
+
+ + 10% +
+ +

Hôm qua

+
+
+ +
+
+
+
32.4k
+

Doanh thu trong tuần này

+
+ +
+ 12% + +
+
+ +
+ +
+
+ + + + + Users Report + + + +
+
+
+ +
+
+
3.4k
+ +
+

Truy cập web trong tuần này

+ + + + 42.5% + +
+
+ +
+
+
Lượt truy cập: +
+
1.458
+
+ +
+
Người xem:
+
2.130
+
+
+ +
+ +
+
+ + + + Leads Report + + +
+
+
\ No newline at end of file diff --git a/old/template/home/home.html b/old/template/home/home.html new file mode 100644 index 0000000..0534409 --- /dev/null +++ b/old/template/home/home.html @@ -0,0 +1,436 @@ + + +
+
+
+
+
+
+
+

+ Đơn hàng mới +

+
+

+ 10.000.000 +

+
+
+
+ +
+
+
+
+ + 10.8% +
+

+ Hôm qua +

+
+
+
+
+
+
+
+

+ Khách hàng liên hệ + qua website +

+
+

+ 1.000.000 +

+
+
+
+ +
+
+
+
+ + 5.4% +
+

+ Hôm qua +

+
+
+
+
+
+
+
+

+ Đơn trả góp +

+
+

+ 600.000 +

+
+
+
+ +
+
+
+
+ + -6.8% +
+

+ Hôm qua +

+
+
+
+
+
+
+
+

+ Đơn hoàn trả +

+
+

+ 30.000 +

+
+
+
+ +
+
+
+
+ + 8.5% +
+

+ Hôm qua +

+
+
+
+
+ +
+
+ + +
+
+
+
+ + Doanh thu + + +
+
+
+ + 184.78K + + + +3.24% + +
+ + Tổng doanh thu trong năm nay + +
+
+
+
+
+ + +
+
+ + +
+
+
+ + Truy cập khách hàng + +
+
+ + Dự đoán + +
+
+
+
+
+

Advertise

+

+ $148 +

+
+ +

4.78%

+
+
+ +
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+ + Lịch sử chỉnh sửa + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Người dùng Nội dung Thời gian Xem chi tiết
1 uyentt@hurasoft.com Chỉnh sửa trang chủ website laptoptcc 24/11/2023 + Xem +
1 uyentt@hurasoft.com Chỉnh sửa trang chủ website laptoptcc 24/11/2023 + Xem +
1 uyentt@hurasoft.com Chỉnh sửa trang chủ website laptoptcc 24/11/2023 + Xem +
1 uyentt@hurasoft.com Chỉnh sửa trang chủ website laptoptcc 24/11/2023 + Xem +
+
+
+
+ +
+
+
+ + +
+
+ Ghi nhắc việc làm cá nhân + +
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Nội dung Hết hạn Trạng thái
1 Sửa trang chủ websiteLaptoptcc 28/11/2023 +
Đang triển khai +
+
2 Sửa trang chủ websiteLaptoptcc 28/11/2023 +
Đang triển khai +
+
3 Sửa trang chủ websiteLaptoptcc 28/11/2023 +
Đang triển khai
+
4 Sửa trang chủ websiteLaptoptcc 28/11/2023 +
Đang triển khai +
+
+
+
+ +
+
\ No newline at end of file diff --git a/old/template/javascript/brand.html b/old/template/javascript/brand.html new file mode 100644 index 0000000..53b156a --- /dev/null +++ b/old/template/javascript/brand.html @@ -0,0 +1,40 @@ + + + + \ No newline at end of file diff --git a/old/template/javascript/edit_template.html b/old/template/javascript/edit_template.html new file mode 100644 index 0000000..a566d74 --- /dev/null +++ b/old/template/javascript/edit_template.html @@ -0,0 +1,83 @@ + + + + + \ No newline at end of file diff --git a/old/template/javascript/global.html b/old/template/javascript/global.html new file mode 100644 index 0000000..e69de29 diff --git a/old/template/javascript/homepage.html b/old/template/javascript/homepage.html new file mode 100644 index 0000000..5e6c200 --- /dev/null +++ b/old/template/javascript/homepage.html @@ -0,0 +1,393 @@ + \ No newline at end of file diff --git a/old/template/javascript/index.html b/old/template/javascript/index.html new file mode 100644 index 0000000..2cbfb78 --- /dev/null +++ b/old/template/javascript/index.html @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + +{% if global.module == 'home' %} + +{% include javascript/homepage %} + +{% elsif global.module == 'product' %} + +{% if global.view == 'home' %} +{% include javascript/product_list %} +{% endif %} + +{% include javascript/product_form %} + +{% elsif global.module == 'deal' %} + +{% include javascript/product_form %} + +{% elsif global.module == 'brand' %} + +{% include javascript/brand %} + +{% elsif global.module == 'report' %} + +{% include javascript/visitor %} + +{% elsif global.module == 'system' %} + +{% include javascript/system %} + +{% elsif global.module == 'page' %} + +{% include javascript/page %} + +{% elsif global.module == 'template' and global.view == 'edit-template' %} + +{% include javascript/edit_template %} + +{% elsif global.module == 'tag' and global.view == 'add' %} + +{% include javascript/tag %} + +{% elsif global.module == 'shipping2' %} + +{% include javascript/shipping2 %} + + +{% endif %} \ No newline at end of file diff --git a/old/template/javascript/page.html b/old/template/javascript/page.html new file mode 100644 index 0000000..0194efd --- /dev/null +++ b/old/template/javascript/page.html @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/old/template/javascript/product_form.html b/old/template/javascript/product_form.html new file mode 100644 index 0000000..9838546 --- /dev/null +++ b/old/template/javascript/product_form.html @@ -0,0 +1,62 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/old/template/javascript/product_list.html b/old/template/javascript/product_list.html new file mode 100644 index 0000000..5211f70 --- /dev/null +++ b/old/template/javascript/product_list.html @@ -0,0 +1,137 @@ + \ No newline at end of file diff --git a/old/template/javascript/shipping2.html b/old/template/javascript/shipping2.html new file mode 100644 index 0000000..57705f9 --- /dev/null +++ b/old/template/javascript/shipping2.html @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/old/template/javascript/system.html b/old/template/javascript/system.html new file mode 100644 index 0000000..4305b4b --- /dev/null +++ b/old/template/javascript/system.html @@ -0,0 +1,73 @@ + + + + + + + \ No newline at end of file diff --git a/old/template/javascript/tag.html b/old/template/javascript/tag.html new file mode 100644 index 0000000..55b57d4 --- /dev/null +++ b/old/template/javascript/tag.html @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/old/template/javascript/visitor.html b/old/template/javascript/visitor.html new file mode 100644 index 0000000..f40f9d0 --- /dev/null +++ b/old/template/javascript/visitor.html @@ -0,0 +1,285 @@ + + \ No newline at end of file diff --git a/old/template/job/applicants.html b/old/template/job/applicants.html new file mode 100644 index 0000000..0de3ebe --- /dev/null +++ b/old/template/job/applicants.html @@ -0,0 +1,97 @@ +
+
+

Danh sách hồ sơ tuyển dụng

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ngày Ứng viên Ghi chú Quản trị Lựa chọn
1 + 09-03-2024 + +

- Họ tên: Hura test

+

- Email: test1@hurasoft.com

+

- Điện thoại: 0987654321

+
+ test + +

+ Đã xử lý

+

bởi test 09-03-2024, 11:01 am

+
+
+ Chi + tiết + + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+ +
+
\ No newline at end of file diff --git a/old/template/job/applicants_detail.html b/old/template/job/applicants_detail.html new file mode 100644 index 0000000..92ae768 --- /dev/null +++ b/old/template/job/applicants_detail.html @@ -0,0 +1,75 @@ +
+ + +
+
+

Chi tiết hồ sơ

+

Phụ trách Chi nhánh Công ty cổ phần Traphaco tại Đồng Nai

+
+ +
+
+ +
+
+
+ Thông tin ứng viên + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Việc ứng tuyểnPhụ trách Chi nhánh Công ty cổ phần Traphaco tại Đồng Nai
Họ tênHura test
Địa chỉ emailtest1@hurasoft.com
Số điện thoại0987654321
File CVtest.docx
Thời gian đăng ký09-03-2024, 10:55 am
Tình trạng xử lý +
+

+ Đã xử lý +

+

bởi test 09-03-2024, 11:01 am

+
+
+ + Cập nhật ghi chú + + + +
+
+
+
+
\ No newline at end of file diff --git a/old/template/job/form.html b/old/template/job/form.html new file mode 100644 index 0000000..6426741 --- /dev/null +++ b/old/template/job/form.html @@ -0,0 +1,305 @@ + + +
+
+
+ {% if global.url contains 'id=' %} +

Sửa nội dung

+ +

+ Phụ trách chi nhánh công ty cổ phần traphaco tại đồng nai +

+ {% else %} +

Thêm mới

+ {% endif %} + + +
+ +
+ +
+ +
+
+

Thông tin cơ bản

+ +
+

Ảnh đại diện

+ +
+ +
+ + +
+ +
+

Ảnh nền trong chi tiết

+ +
+ +
+ + +
+ +
+

Danh mục * có thể chọn nhiều danh + mục 

+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+

Tiêu đề

+ + +
+ +
+

Link Index

+ + +
+ +
+

Link tham khảo (link web ngoài nếu có)

+ + +
+ +
+

Tóm tắt chính

+ + +
+ +
+

Tags (Mỗi cụm từ 1 dòng)

+ + +
+
+ + +
+
+

Mô tả  công việc

+
+ +
+ +
+ +
+

Thu nhập + V.d Từ 8 triệu/ + Thỏa thuận

+ + +
+ + +
+
+

Số lượng cần tuyển + Nhập số lượng v.d. 2

+ + +
+
+

Hạn ứng tuyển + V.d Từ 8 triệu/ + Thỏa thuận

+ +
+ + +
+
+
+ +
+

Địa điểm

+ + +
+
+ + +
+

Dùng cho SEO

+ + +
+

+ Meta Title +  0 ký tự , 0 từ +

+ + +
+ +
+

+ Meta Keywords +  0 ký tự , 0 từ +

+ + +
+ +
+

+ Meta Description + 0 ký tự , 0 từ (Khuyến nghị: 160 ký tự) +

+ + +
+ +
+

Trạng thái

+ +
+ + + + + + +
+
+ +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/old/template/job/home.html b/old/template/job/home.html new file mode 100644 index 0000000..2998b42 --- /dev/null +++ b/old/template/job/home.html @@ -0,0 +1,343 @@ +
+
+

Tuyển dụng

+ +
+ + +
+ +
+
+ + + + + Xem nội dung đang ẩn + + + + Xem nội dung nổi bật + +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Danh sách Số liệu Quản trị Lựa chọn
1 + + + Phụ trách Chi nhánh Công ty cổ phần Traphaco tại Đồng Nai +
+ STT + +
+
+

- Số hồ sơ: 1

+

- Lượt xem: 0

+
+

Cập nhật: Admin

+

(15-02-2024, 2:57 pm)

+
+
+ + + + + + + + + + + + + + +
+
1 + + + Phụ trách Chi nhánh Công ty cổ phần Traphaco tại Đồng Nai +
+ STT + +
+
+

- Số hồ sơ: 1

+

- Lượt xem: 0

+
+

Cập nhật: Admin

+

(15-02-2024, 2:57 pm)

+
+
+ + + + + + + + + + + + + + +
+
1 + + + Phụ trách Chi nhánh Công ty cổ phần Traphaco tại Đồng Nai +
+ STT + +
+
+

- Số hồ sơ: 1

+

- Lượt xem: 0

+
+

Cập nhật: Admin

+

(15-02-2024, 2:57 pm)

+
+
+ + + + + + + + + + + + + + +
+
1 + + + Phụ trách Chi nhánh Công ty cổ phần Traphaco tại Đồng Nai +
+ STT + +
+
+

- Số hồ sơ: 1

+

- Lượt xem: 0

+
+

Cập nhật: Admin

+

(15-02-2024, 2:57 pm)

+
+
+ + + + + + + + + + + + + + +
+
1 + + + Phụ trách Chi nhánh Công ty cổ phần Traphaco tại Đồng Nai +
+ STT + +
+
+

- Số hồ sơ: 1

+

- Lượt xem: 0

+
+

Cập nhật: Admin

+

(15-02-2024, 2:57 pm)

+
+
+ + + + + + + + + + + + + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+ +
+
\ No newline at end of file diff --git a/old/template/marketing/conditional_promotion.html b/old/template/marketing/conditional_promotion.html new file mode 100644 index 0000000..a57facf --- /dev/null +++ b/old/template/marketing/conditional_promotion.html @@ -0,0 +1,79 @@ +
+
+

Khuyến mại cấu hình máy tính

+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Khuyến mại Điều kiện áp dụng Thông tin khác Lựa chọn
1 +

20.000.000

+

- Tặng tiền mặt: 500.000

+

- Tùy chọn gói sản phẩm: 500.000

+

- Nội dung:Tặng ngay tiền mặt 500.000 VNĐ

+
+

1. Bắt buộc phải có sản phẩm thuộc các danh mục: 30,31,32,164,35,36

+

2. Giá trị đơn hàng trong khoảng: 20.000.000 VND - 30.000.000 VND

+
+

Tạo bởi: Nguyễn Tiến Dinh (10-11-2023, 10:06 am)

+

Cập nhật: Nguyễn Tiến Dinh (18-12-2023, 2:59 pm)

+
+
+ + + + + + + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
+
\ No newline at end of file diff --git a/old/template/marketing/conditional_promotion_add.html b/old/template/marketing/conditional_promotion_add.html new file mode 100644 index 0000000..c78f9cb --- /dev/null +++ b/old/template/marketing/conditional_promotion_add.html @@ -0,0 +1,141 @@ + + +
+
+

Cập nhật khuyến mại

+ + + + + Thêm khuyến mại + + +

+ 100 triệu +

+
+ + + +
+ + + + + + + + + + + + + + + + + + + + +
Tên gọi + + * Phần này chỉ dùng cho quản trị viên phân biệt. +
Nội dung khuyến mại + +
+

1. Tặng tiền mặt: VND

+ +
+ +
+

2. Tùy chọn gói sản phẩm: VND

+ +
+ +
+

3. Nội dung:

+ +
+ +
Điều kiện áp dụng * + +
+

1. Bắt buộc phải có sản phẩm thuộc các danh mục:

+ +
+ +

2. Giá trị đơn hàng trong khoảng: VND +

+ +
+
+

Giá từ

+ +
+ +
+

Đến

+ +
+
+ +
Hiển thị + + + + +
+ + +
+
+ + + + + + + \ No newline at end of file diff --git a/old/template/marketing/coupon.html b/old/template/marketing/coupon.html new file mode 100644 index 0000000..c53f3d6 --- /dev/null +++ b/old/template/marketing/coupon.html @@ -0,0 +1,157 @@ +
+
+

Coupon khuyến mại

+ +
+

Danh sách phiếu giảm giá/ quà tặng dùng cho khách hàng khi đặt mua hàng từ + website. + Trước khi gửi đơn hàng, nếu khách hàng có mã số phiếu còn hiệu lực thì sẽ nhập vào đơn hàng để nhận được + ưu + đãi ghi trên phiếu

+ + + + + Thêm phiếu mới + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Mã số Khuyến mại Phân loại Thời gian Trạng thái Thông tin khác Lựa chọn
1 CP231020233 +

Test phiếu giảm giá

+
Test phiếu giảm giá
+
+

Tặng sản phẩm

+

Balo thời trang

+
+ +
+

+ - Bắt đầu: 23-10-2023, 12:00 am
+ - Kết thúc: 28-10-2023, 12:00 am +

+
+ + Đã chạy + + + + Đã dừng + +
+
+
+

0 đã dùng

+

Tổng số: 20

+
+
+

- Tạo bởi: Đức (05-09-2022, 9:48 am)

+

- Cập nhật cuối: hongvt@hurasoft.com
(03-10-2023, 8:31 am)

+

- Giá trị đơn hàng tối thiểu: 2.000.000 đ +

+
+
+
+ + + + + + + + +
+
1 CP231020233 +

Test phiếu giảm giá

+
Test phiếu giảm giá
+
+

Tặng sản phẩm

+

Balo thời trang

+
+
+

+ - Bắt đầu: 23-10-2023, 12:00 am
+ - Kết thúc: 28-10-2023, 12:00 am +

+
+ + Đã chạy + + + + Đã dừng + +
+
+
+

0 đã dùng

+

Tổng số: 20

+
+

- Tạo bởi: Đức (05-09-2022, 9:48 am)

+

- Cập nhật cuối: hongvt@hurasoft.com
(03-10-2023, 8:31 am)

+

- Giá trị đơn hàng tối thiểu: 2.000.000 đ

+
+
+ + +
+
+
+
+
\ No newline at end of file diff --git a/old/template/marketing/coupon_add.html b/old/template/marketing/coupon_add.html new file mode 100644 index 0000000..bef7109 --- /dev/null +++ b/old/template/marketing/coupon_add.html @@ -0,0 +1,668 @@ + + +
+
+ {% if global.url contains 'id=' %} +
+

Cập nhật

+ + + + + Thêm phiếu mới + + +

+ Giảm 10.000đ khi mua đặt hàng online (nhập mã test2023) +

+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Mã số phiếu * CP220920213
Tên phiếu khuyến mại * + + * Phần này sẽ hiển thị trong đơn hàng của khách hàng, + do vậy cần viết cụ thể. +
Mô tả + + + Phần này chỉ dành cho admin nắm được cụ thể khuyến mại + này làm gì. Khách hàng không nhìn thấy. +
Phân loại * +
+ + +
+ Tên Sản phẩm + +
+ + + + +
+
Thời gian hiệu lực * + +
+

Bắt đầu

+
+ + +
+ +

Giờ

+ + +
+ +
+

Kết thúc

+
+ + +
+ +

Giờ

+ + +
+
Giá trị đơn hàng tối thiểu + + vnd (để 0 nếu không áp dụng) +
Giới hạn số lần sử dụng / 1 khách hàng + + (để 0 nếu không giới hạn số lượng dùng phiếu này trên 1 khách hàng) +
Áp dụng đồng thời với các mã KM khác + +
Hiển thị công khai + +
Điều kiện áp dụng + +
+ +
Số lượng phiếu + + (để 0 nếu không có giới hạn) +
Hiển thị + + + + +
+ + + +
+ + {% else %} + +

+ Thêm / sửa coupon

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Mã số phiếu * + + + + + + + +
Tên phiếu khuyến mại * + + * Phần này sẽ hiển thị trong đơn hàng của khách hàng, + do vậy cần viết cụ thể. +
Mô tả + + + Phần này chỉ dành cho admin nắm được cụ thể khuyến mại + này làm gì. Khách hàng không nhìn thấy. +
Phân loại * +
+ + +
+ Tên Sản phẩm + +
+ + + + +
+
Thời gian hiệu lực * + +
+

Bắt đầu

+
+ + +
+ +

Giờ

+ + +
+ +
+

Kết thúc

+
+ + +
+ +

Giờ

+ + +
+
Giá trị đơn hàng tối thiểu + + vnd (để 0 nếu không áp dụng) +
Giới hạn số lần sử dụng / 1 khách hàng + + (để 0 nếu không giới hạn số lượng dùng phiếu này trên 1 khách hàng) +
Áp dụng đồng thời với các mã KM khác + +
Hiển thị công khai + +
Điều kiện áp dụng + +
+ + +
Số lượng phiếu + + (để 0 nếu không có giới hạn) +
Hiển thị + + + + +
+ + + +
+ + {% endif %} +
+
+ + + \ No newline at end of file diff --git a/old/template/marketing/coupon_product.html b/old/template/marketing/coupon_product.html new file mode 100644 index 0000000..9851bb4 --- /dev/null +++ b/old/template/marketing/coupon_product.html @@ -0,0 +1,116 @@ + + +
+
+
+

Cập nhật

+ + + + + Thêm phiếu mới + + +

+ Giảm 10.000đ khi mua đặt hàng online (nhập mã test2023) +

+
+ + + +
+ +
+

Danh sách sản phẩm áp dụng phiếu giảm giá

+ + + + + Thêm sản phẩm mới + +
+ + +
+ + +

Chọn danh mục

+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Tên Thông tin bán hàng Lựa chọn
1 + + + Mainboard MSI B450 TOMAHAWK MAX II (AMD B450, Socket AM4, ATX, 4 Khe Cắm Ram DDR4) + +

- Mã kho: CN1

+

- Giá bán: 1.999.000 vnđ

+

- Kho hàng : 1

+

- Bảo hành : 36 Tháng

+
+
+ + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
+
+
\ No newline at end of file diff --git a/old/template/marketing/detail.html b/old/template/marketing/detail.html new file mode 100644 index 0000000..281c686 --- /dev/null +++ b/old/template/marketing/detail.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/old/template/marketing/feed.html b/old/template/marketing/feed.html new file mode 100644 index 0000000..99e79d0 --- /dev/null +++ b/old/template/marketing/feed.html @@ -0,0 +1,108 @@ +{% if global.url contains 'view=fb-product-select' %} + +{% include 'marketing/feed_select' %} + +{% elsif global.url contains 'view=fb-product-add' %} + +{% include 'marketing/feed_add' %} + +{% elsif global.url contains 'view=fb-product-list' %} + +{% include 'marketing/feed_list' %} + +{% else %} +
+
+

Facebook Product Ads

+ +
+

Tổng hợp các danh sách chứa Sản phẩm dùng quảng cáo trên Facebook. Dùng link Facebook + Feed + ở danh sách phù hợp để nhập trong facebook ad manager

+ + + + + Thêm danh sách mới + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Danh sách Sản phẩm Facebook feed Google feed Cập nhật Lựa chọn
1 2023 còn hàng 6.055 sp +
+ + FB feed +
+
+ + +

Trương Thị Thái Ninh

+

25-10-2023, 3:05 pm

+
+
+ Xem + sản phẩm + + + + + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
+
+{% endif %} \ No newline at end of file diff --git a/old/template/marketing/feed_add.html b/old/template/marketing/feed_add.html new file mode 100644 index 0000000..c3fabc8 --- /dev/null +++ b/old/template/marketing/feed_add.html @@ -0,0 +1,70 @@ + + +
+
+
+

Cập nhật nhóm FB ad

+

ALL SP WEB 2023 NEW

+
+ + + +
+
+

Thông tin cơ bản

+ + + Chọn Sản phẩm vào danh sách + +
+ +
+ Đặt tên danh sách + + +
+ + +
+

Ghi chú / mô tả

+ + +
+ + +
+
+

+ Google Product Category ID + Chỉ điền 1 danh mục: ID hoặc Tên danh mục trong danh sách  + tại đây +

+ + V.d nếu là sp Bird Food thì điền 4990 hoặc Animals & Pet Supplies > + Pet Supplies > Bird Supplies > Bird Food +
+ + +
+ +
+ + +
+
+
\ No newline at end of file diff --git a/old/template/marketing/feed_list.html b/old/template/marketing/feed_list.html new file mode 100644 index 0000000..c670d5c --- /dev/null +++ b/old/template/marketing/feed_list.html @@ -0,0 +1,113 @@ + + +
+

Danh sách: 2023 còn hàng

+ +
+ + +
+ +
+ + Xóa danh sách được chọn + + +
+ + + + + + Thêm sản phẩm + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Tên Sản phẩm Thông tin bán hàng Lựa chọn
+ + 1 + + + Chuột game Steelseries Prime Mini (62421) (USB/RGB) +

Mã kho:  MOVI213

+

Lượt xem: 3360

+

Cập nhật: Hôm nay, 8:59 am

+ +

Chưa hiển thị

+
+

Giá: 999.000 vnđ

+

Kho hàng: 5

+

Bảo hành: 36 Tháng

+
+ + Loại sản phẩm này + +
2 Chuột game Steelseries

Mã kho:  MOVI213

Lượt xem: 3360

Cập nhật: Hôm nay, 8:59 am

Chưa hiển thị

Giá: 999.000 vnđ

Kho hàng:

Bảo hành:

Loại sản phẩm này
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
+ + + + \ No newline at end of file diff --git a/old/template/marketing/feed_select.html b/old/template/marketing/feed_select.html new file mode 100644 index 0000000..875fdce --- /dev/null +++ b/old/template/marketing/feed_select.html @@ -0,0 +1,83 @@ + + +
+

Chọn sản phẩm hiển thị Facebook

+ +
+ + +
+ +
+ + +
+ + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Tên Sản phẩm Thông tin bán hàng Lựa chọn
1 + Chuột game Steelseries Prime Mini (62421) (USB/RGB) +

Mã kho: MOVI213

+

Lượt xem: 3.360

+

Cập nhật: Hôm nay, 8:59 am

+
+

Giá: 999.000 vnđ

+

Kho hàng: 5

+

Bảo hành: 36 Tháng

+
+ Chọn sản phẩm +
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
\ No newline at end of file diff --git a/old/template/marketing/home.html b/old/template/marketing/home.html new file mode 100644 index 0000000..281c686 --- /dev/null +++ b/old/template/marketing/home.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/old/template/marketing/poster_list.html b/old/template/marketing/poster_list.html new file mode 100644 index 0000000..6b5c170 --- /dev/null +++ b/old/template/marketing/poster_list.html @@ -0,0 +1,45 @@ +
+

Danh sách poster quảng cáo

+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Quản trị Tên file Danh sách file Thời gian đăng Lựa chọn
1 admin@hurasoft.com Huyen - Test - Bỉm (Mẹ & Bé).html 02-08-2023, 4:51 pm
2 admin@hurasoft.com Huyen - Test - Bỉm (Mẹ & Bé).html 02-08-2023, 4:51 pm
+
+
\ No newline at end of file diff --git a/old/template/marketing/poster_upload.html b/old/template/marketing/poster_upload.html new file mode 100644 index 0000000..a892416 --- /dev/null +++ b/old/template/marketing/poster_upload.html @@ -0,0 +1,48 @@ + + +
+ + Chức năng này cho phép bạn upload các file quảng cáo poster. + Yêu cầu cần đặt trong file đuôi .zip. Server này giới hạn dung lượng file upload là 20 MB, do vậy nếu file .zip + dung lượng lớn hơn 20MB thì bạn cần sử dụng công cụ FTP (hoặc liên hệ bên cung cấp server để nâng giới hạn file + upload). + +

+ Upload poster quảng cáo

+ +
+ + +
+

File zip chỉ được phép chứa file ảnh (jpg, gif) và html nếu có.

+ +

+ Chú ý: Bạn cần nhớ tên file để truy cập, ví dụ bạn upload file abc.zip có + chứa file khuyenmai.html, thì sau khi upload xong, đường dẫn truy cập sẽ là + https://demopc8.hurasoft.com/media/marketing/abc/khuyenmai.html +

+ +
+

Chọn file zip để upload (dung lượng tối đa 20MB)

+ +
+ + +
+
+
\ No newline at end of file diff --git a/old/template/marketing/product_offer.html b/old/template/marketing/product_offer.html new file mode 100644 index 0000000..c35a0f0 --- /dev/null +++ b/old/template/marketing/product_offer.html @@ -0,0 +1,63 @@ +
+
+ +
+

Sản phẩm phiếu khuyến mại

+ + + + Tải danh sách hiện tại + + +

Hướng dẫn: Để cập nhật/xoá Sản phẩm có mã khuyến mại, tải file + excel mới nhất về và sửa từ đó. Để xoá sp thì chỉ cần xoá sp khỏi file excel. Sau khi chỉnh xong file + excel thì Upload lại để hệ thống nhận mới.

+
+ + +
+ +
+ + +
+ + + + + + + + + + + + + +
File cập nhật + +
Lựa chọn + + + + (* Nếu lựa chọn, hệ thống sẽ xóa bỏ hết dữ liệu đang có trước khi + cập nhật dữ liệu nhận được từ file excel) +
+ + +
+
+
\ No newline at end of file diff --git a/old/template/marketing/promotion.html b/old/template/marketing/promotion.html new file mode 100644 index 0000000..4b1c2bd --- /dev/null +++ b/old/template/marketing/promotion.html @@ -0,0 +1,95 @@ +
+
+

Khuyến mại theo sản phẩm

+ +
+ + + + + +
+ +
+

Danh sách các khuyến mại đi kèm khi người dùng mua Sản phẩm. Bạn cần tạo khuyến mại và lựa chọn Sản phẩm + phù + hợp cho từng khuyến mại

+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STT Khuyến mại Thời gian Cập nhật cuối Sản phẩm Lựa chọn
1 +

+ [Quà tặng] Phiếu mua hàng trị giá 100.000 VND +

+

- Mô tả: Phiếu mua hàng

+

- Giá trị: 0 VND

+

- Số lượng: 0

+
+

Không giới hạn thời gian

+
+

Admin

+

06-06-2020, 10:57 am

+
+ Xem + +
+ + + + + + +
+
+
+
+
\ No newline at end of file diff --git a/old/template/marketing/promotion_add.html b/old/template/marketing/promotion_add.html new file mode 100644 index 0000000..28abf76 --- /dev/null +++ b/old/template/marketing/promotion_add.html @@ -0,0 +1,278 @@ + + +
+
+ {% if global.url contains 'id=' %} +
+

Cập nhật

+ + + + + Thêm khuyến mại mới + + +

+ Tặng phiếu vệ sinh bảo dưỡng Laptop, PC miễn phí trọn đời trị giá 999.000đ (THEK417) +

+
+ + + + + {% endif %} + +
+ +

Thông tin cơ bản

+ +
+

Hình thức

+ + +
+ +
+

Ảnh đại diện (nếu có)

+ + +
+ +
+
+ +
+

Đặt tiêu đề khuyến mại

+ + +
+ +
+
+

Giá trị bằng (VNĐ)

+ + +
+ +
+

Số lượng (Để = -1 nếu không hạn chế số lượng)

+ +
+
+ +
+

Mô tả

+ + +
+ +
+

Thời gian hiệu lực

+ +
+ + + + + +
+

Bắt đầu

+
+ + +
+ +

Giờ

+ + +
+ +
+

Kết thúc

+
+ + +
+ +

Giờ

+ + +
+
+
+ +
+

Link liên quan (nếu có) (Link tới 1 bài viết giới thiệu chi tiết hoặc link + sản phẩm)

+ + +
+ +
+

Ghi chú

+ + +
+ +
+

Hiển thị

+ + + + + + +
+ +

+ + +
+
+
\ No newline at end of file diff --git a/old/template/marketing/promotion_group.html b/old/template/marketing/promotion_group.html new file mode 100644 index 0000000..678ba84 --- /dev/null +++ b/old/template/marketing/promotion_group.html @@ -0,0 +1,84 @@ + + +
+

Nhóm khuyến mại

+ +
+

Tổ chức khuyến mại theo nhóm để lựa chọn cho sản phẩm nhanh hơn.

+ + + + + Thêm nhóm mới + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Nhóm Danh sách khuyến mại Ghi chú Cập nhật cuối Sản phẩm Lựa chọn
1 + KM màn +

- Mô tả:

+

- Phân loại: Khách hàng được nhận tất cả KM trong nhóm

+
+

- Đón Tết Rồng Vàng Tặng 2024 Phần Quà (0 VND)

+

- KM Giá treo màn hình (0 VND)

+
+

Trần Kim Ngân

+

05-02-2024, 10:15 am

+
+ + Xem sản phẩm + + +
+ + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+ +
diff --git a/old/template/marketing/promotion_group_add.html b/old/template/marketing/promotion_group_add.html new file mode 100644 index 0000000..816132d --- /dev/null +++ b/old/template/marketing/promotion_group_add.html @@ -0,0 +1,187 @@ + + +
+
+ {% if global.url contains 'id=' %} +
+

Cập nhật

+ + + + + Thêm nhóm mới + + +

+ Tặng phiếu vệ sinh bảo dưỡng Laptop, PC miễn phí trọn đời trị giá 999.000đ (THEK417) +

+
+ + + {% endif %} + +
+

Thông tin cơ bản

+ +
+

Đặt tên nhóm

+ + +
+ +
+

Mô tả

+ + +
+ +
+

Chọn khuyến mại

+ + + Chọn khuyến mại vào nhóm + + + (* Cần nhấn Cập nhật để lưu lại) +
+ +
+ + + + + + + + + + + + + + + + + + + +
Khuyến mại Lựa chọn
- Tặng ngay 01 Bếp lẩu nướng đa năng- hiệu Edra +
+ + + + + + +
+
- Test Khuyến mại 2
- Test 3
+
+ + +
+

Ghi chú (* dành cho quản trị)

+ + +
+ +
+ + + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/old/template/marketing/promotion_group_product.html b/old/template/marketing/promotion_group_product.html new file mode 100644 index 0000000..da7fd26 --- /dev/null +++ b/old/template/marketing/promotion_group_product.html @@ -0,0 +1,290 @@ + + +
+
+ {% if global.url contains 'id=' %} +
+

Cập nhật

+ + + + + Thêm nhóm mới + + +

+ Tặng phiếu vệ sinh bảo dưỡng Laptop, PC miễn phí trọn đời trị giá 999.000đ (THEK417) +

+
+ + + {% endif %} + +
+
+

Danh sách sản phẩm có nhóm khuyến mại

+ + + + + Thêm sản phẩm mới + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Tên Thông tin bán hàng Lựa chọn
1 + + + Mainboard MSI B450 TOMAHAWK MAX II (AMD B450, Socket AM4, ATX, 4 Khe Cắm Ram DDR4) +

Mã kho : Hurasoft212333

+
+

- Giá bán: 1.999.000 vnđ

+

- Kho hàng : 1

+

- Bảo hành : 36 Tháng

+
+
+ + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/old/template/marketing/promotion_product.html b/old/template/marketing/promotion_product.html new file mode 100644 index 0000000..e59c47e --- /dev/null +++ b/old/template/marketing/promotion_product.html @@ -0,0 +1,290 @@ + + +
+
+ {% if global.url contains 'id=' %} +
+

Cập nhật

+ + + + + Thêm khuyến mại mới + + +

+ Tặng phiếu vệ sinh bảo dưỡng Laptop, PC miễn phí trọn đời trị giá 999.000đ (THEK417) +

+
+ + + {% endif %} + +
+
+

Danh sách sản phẩm có nhóm khuyến mại

+ + + + + Thêm sản phẩm mới + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Tên Thông tin bán hàng Lựa chọn
1 + + + Mainboard MSI B450 TOMAHAWK MAX II (AMD B450, Socket AM4, ATX, 4 Khe Cắm Ram DDR4) +

Mã kho : Hurasoft212333

+
+

- Giá bán: 1.999.000 vnđ

+

- Kho hàng : 1

+

- Bảo hành : 36 Tháng

+
+
+ + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/old/template/media/category.html b/old/template/media/category.html new file mode 100644 index 0000000..9de3ad5 --- /dev/null +++ b/old/template/media/category.html @@ -0,0 +1,52 @@ +
+ + + +
+

Phân loại nhóm danh mục media

+ + + + + Thêm nhóm mới + + + + + + + + + + + + + + + + + + + + + +
STTIDTên nhómThời gianLựa chọn
11385Ảnh tin tức2023-10-01 12:00:00 +
+ + +
+
+ +
+
\ No newline at end of file diff --git a/old/template/media/home.html b/old/template/media/home.html new file mode 100644 index 0000000..60b84ae --- /dev/null +++ b/old/template/media/home.html @@ -0,0 +1,102 @@ +
+
+

Danh sách media

+ +
+ + +
+ +
+
+ + + + Quản lý nhóm media + +
+ + + + + Thêm nội dung mới + +
+ +

+ Hướng dẫn: Chức năng này cho phép bạn quản lý tất cả các loại file media (như file word, excel, ảnh + chương trình, ảnh banner, file flash, ảnh Sản phẩm ...) phục vụ cho website. Copy đường dẫn file và gắn vào + miêu + tả Sản phẩm hay bài viết khác. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Xem nhanh Tên file Loại file Thời gian Lựa chọn
1 + + Xem nhanh + + +
+

Đường dẫn

+ + +
+
banner 2023-08-11 00:00:00 +
+ + + +
+ +
+

STT

+ + +
+
+
+ +
+
\ No newline at end of file diff --git a/old/template/media/upload.html b/old/template/media/upload.html new file mode 100644 index 0000000..29cce6c --- /dev/null +++ b/old/template/media/upload.html @@ -0,0 +1,129 @@ +
+ + + +
+
+
+

Cập nhật media

+ + + + Thêm file Media mới + +
+

/media/lib/11-08-2023/new.png

+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Link file + +
Đặt tên + +
Phân nhóm +
+ + + Quản lý nhóm media + +
+
Chiều rộng (width) +
+ + (điền số cho đơn vị pixel, bắt buộc với + Flash, Video + clip) +
+
Chiều Cao (height) +
+ + (điền số cho đơn vị pixel, bắt buộc với + Flash, Video + clip) +
+
File ảnh + +
+

Upload file

+ +
+ Dung lượng file phải nhỏ hơn 20MB +
Code HTML + +
Hoặc Link trên mạng + +
+ + +
+
+
+ +
+ +
\ No newline at end of file diff --git a/old/template/module_css/homepage.html b/old/template/module_css/homepage.html new file mode 100644 index 0000000..5d071ae --- /dev/null +++ b/old/template/module_css/homepage.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/old/template/module_css/index.html b/old/template/module_css/index.html new file mode 100644 index 0000000..8f2961c --- /dev/null +++ b/old/template/module_css/index.html @@ -0,0 +1,5 @@ +{% if global.module == 'home' %} + +{% include module_css/homepage %} + +{% endif %} \ No newline at end of file diff --git a/old/template/order/bargain.html b/old/template/order/bargain.html new file mode 100644 index 0000000..e10be38 --- /dev/null +++ b/old/template/order/bargain.html @@ -0,0 +1,100 @@ +
+

Danh sách mặc cả giá

+ + +

+ Tổng số mặc cả giá nhận được: 1.458 +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STT Thời gian Khách hàng Thông tin sản phẩm Trạng thái
1 16-11-2023, 9:59 am +

- Họ tên: Ngô Quang Sơn

+

- Email: sonnq57@yahoo.com.sg

+

- Tel: 0913236989

+
+

- Sản phẩm: Tai nghe NICEHCK Traceless có Mic (Type-C)

+

- Giá bán (lúc mặc cả): 258.850.000

+

- Giá mặc cả: 0

+

- Nguồn mặc cả: https://hacom.vn/

+
+
+ Thành công + Chưa xử lý + Hủy bỏ + + +
+ +

Lúc 28-02-2015, 12:40 pm

+
+ +
+
+ +
+ + + + 1 + 2 + 3 + 4 + 5 + + + +
+
+ + \ No newline at end of file diff --git a/old/template/order/detail.html b/old/template/order/detail.html new file mode 100644 index 0000000..39bd351 --- /dev/null +++ b/old/template/order/detail.html @@ -0,0 +1,514 @@ + + +
+ +
+ +
+ Đơn hàng #000-368 + +
+

Thời gian: 16-11-2023, 9:59 am

+
+ +
+
+
+ 1. Khách hàng + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Tên Nguyen Duc Quan
Số điện thoại 0822256369
Email quannd@hurasoft.com
Địa chỉ nhận hàng + Kiến Hưng, Quận Hà Đông, Hà Nội + +
Địa chỉ thanh toán + Kiến Hưng, Quận Hà Đông, Hà Nội + +
+
+
+ + +
+ 2. Sản phẩm + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
STT Mã kho Tên Giá SL Tổng Ghi chú
1 HH0003 + HHWS - CORE i9 9900X | 32G | + NVIDIA RTX 2060 6GB + 34.800.000 VND (chiếc) 2 69.600.000 VND Ghi chú
+
+
+ + +
+ 3. Số tiền + +
+ + + + + + + + + + + + + + + + + + + + +
Tổng giá trị + 69.600.000 VND +
Phí vận chuyển & giao hàng + 0 VND + +
Phí thu hộ + 0 VND + +
Tổng thu 69.600.000 VND
+
+
+ + +
+ 4. Lịch sử cập nhật + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
STT Phân loại Trạng thái Ghi chú Người cập nhật
1 payment paid test Đức lúc 22-05-2023, 4:49 pm
Hiện tại chưa có trạng thái nào được cập nhật
+
+
+
+ +
+
+

Nhân sự xử lý đơn hàng

+

+ Uyên Hura + +

+ + +
+ +
+

Trạng thái thanh toán

+ +
+ + Chưa thanh toán + + +
+ + + +

Khách chọn: Thanh toán bằng VNPAY

+
+ +
+

Trạng thái vận chuyển

+ +

+ + Chưa chuyển + + +

+ + + +

Khách chọn: Giao hàng tiêu chuẩn

+
+ +
+

+ Trạng thái đơn hàng + +

+ + +
+ +
+

Ghi chú

+ + + +
+ + +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/old/template/order/detail_new.html b/old/template/order/detail_new.html new file mode 100644 index 0000000..e69de29 diff --git a/old/template/order/home.html b/old/template/order/home.html new file mode 100644 index 0000000..a24c6d2 --- /dev/null +++ b/old/template/order/home.html @@ -0,0 +1,196 @@ +
+

Danh sách đơn hàng

+ + + + + +
+
+

+ Tổng số đơn hàng: 1.458 +

+ +

+ Tổng giá trị: 14.948.348.000 VNĐ +

+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Mã số Thời gian Khách hàng Tỉnh/TP Giá trị + + + + + + + + Xem
1 + 000-369 + 16-11-2023, 9:59 am + Nguyen Duc Quan + Hà Nội 69.600.000 +
Thành công
+
+
Chờ xử lý
+
+
Lỗi
+
+ Nguyen Duc Quan + +
+ + +
+
1 + 000-369 + 16-11-2023, 9:59 am + Nguyen Duc Quan + Hà Nội 69.600.000 +
Thành công
+
+
Chờ xử lý
+
+
Đang xử lý
+
+ Nguyen Duc Quan + +
+ + +
+
+
+ +
+ + + + 1 + 2 + 3 + 4 + 5 + + + +
+
\ No newline at end of file diff --git a/old/template/order/paygate.html b/old/template/order/paygate.html new file mode 100644 index 0000000..f329566 --- /dev/null +++ b/old/template/order/paygate.html @@ -0,0 +1,131 @@ +
+

Thanh toán qua cổng dịch vụ

+ + + +
+ + +
+

Từ ngày

+ + +
+
+

Đến ngày

+ +
+ + +
+ +
+
+

Tổng số: 8

+

Tổng giá trị: 800.000 VNĐ

+
+ + + + Tải file excel + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT ID Cổng Thời gian Khách hàng Thanh toán cho Giá trị Trạng thái
1 10 viettelpay 06-11-2022, 11:26 am +

- Họ tên: Ngô Quang Sơn

+

- Email: sonnq57@yahoo.com.sg

+

- Tel: 0913236989

+
[order] 123 100.000 + Thành công + + + +
+
+ +
+ + + + 1 + 2 + 3 + 4 + 5 + + + +
+
+ + \ No newline at end of file diff --git a/old/template/order/wait_order.html b/old/template/order/wait_order.html new file mode 100644 index 0000000..b4d0a62 --- /dev/null +++ b/old/template/order/wait_order.html @@ -0,0 +1,114 @@ +
+

Đặt chờ sản phẩm

+ + + +
+ + +
+ +
+

Danh sách các Sản phẩm khách hàng chờ đặt hàng

+ + + + Tải file excel + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Khách hàng Sản phẩm Nhận hàng tại Ghi chú Trạng thái
1 +

- Họ tên: Ngô Quang Sơn

+

- Email: sonnq57@yahoo.com.sg

+

- Tel: 0913236989

+
+ [Mới 100%] Dell Gaming G16 7620 (Intel core + i7-12700H, 16GB, 1TB, + RTX 3060 6GB, 16″ QHD+ 165Hz IPS) + 20 Láng Hạ, Đống Đa, Hà Nội Nhận hàng giờ hành chính +
+ Thành công + Chưa xử lý + Hủy bỏ + + +
+ +

Lúc 28-02-2015, 12:40 pm

+
+ +
+
+ +
+ + + + 1 + 2 + 3 + 4 + 5 + + + +
+
+ + \ No newline at end of file diff --git a/old/template/other/header.html b/old/template/other/header.html new file mode 100644 index 0000000..70fafe3 --- /dev/null +++ b/old/template/other/header.html @@ -0,0 +1,409 @@ + \ No newline at end of file diff --git a/old/template/other/topbar.html b/old/template/other/topbar.html new file mode 100644 index 0000000..c30a1c6 --- /dev/null +++ b/old/template/other/topbar.html @@ -0,0 +1,508 @@ + + + + + \ No newline at end of file diff --git a/old/template/page/category.html b/old/template/page/category.html new file mode 100644 index 0000000..85eadc2 --- /dev/null +++ b/old/template/page/category.html @@ -0,0 +1,82 @@ +
+ + +
+

Danh mục nội dung cố định

+ + + + + Thêm danh mục mới + + + + + + + + + + + + + + + + + + + + + + + + + + +
Danh mụcIDSTTLựa chọn
1.Chính sách chung2016 + + + +
1.Chính sách chung2016 + + + +
+
+
\ No newline at end of file diff --git a/old/template/page/category_add.html b/old/template/page/category_add.html new file mode 100644 index 0000000..2c7a2c5 --- /dev/null +++ b/old/template/page/category_add.html @@ -0,0 +1,123 @@ +
+ + +
+
+
+

Cập nhật danh mục

+ +
+

Chính sách chung

+
+ +
+
+ +
+
+
+ Thông tin cơ bản + +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ +
+ + + Upload ảnh + +

No file chosen

+
+
+ +
+ +
+ Dùng cho SEO + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+
+ +
+ +
\ No newline at end of file diff --git a/old/template/page/form.html b/old/template/page/form.html new file mode 100644 index 0000000..a5d28c4 --- /dev/null +++ b/old/template/page/form.html @@ -0,0 +1,156 @@ +
+ + +
+
+
+

Cập nhật nội dung

+ +
+

Chương trình khuyến mãi tháng

+
+ +
+
+ +
+
+
+ Thông tin cơ bản + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + + Quản lý danh mục + +
+
+ +
+ +
+ + + Upload ảnh + +

No file chosen

+
+
+ +
+ +
+ Nội dung chi tiết +
+ + +
+ +
+

Hoặc nhập nội dung

+ +
+ + + + +
+ +
+ Dùng cho SEO + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+
+ +
+ +
\ No newline at end of file diff --git a/old/template/page/home.html b/old/template/page/home.html new file mode 100644 index 0000000..8eff4b5 --- /dev/null +++ b/old/template/page/home.html @@ -0,0 +1,187 @@ +
+
+

Danh sách nội dung cố định

+ + + +
+ Hướng dẫn : Phần này tổng hợp các nội dung cố định cung cấp cho khách hàng (v.d. các hướng dẫn mua + hàng, quy + định, chính + sách bán hàng, vv..) +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTDanh sáchThời gianLượt xemLựa chọn
1 +
+ [#188] HACOM Sinh nhật 22 tuổi cùng Acer +
+
+

URL

+ +
+
+ Cập nhật: 29-12-2023, 9:56 am + + 0 + + +
1 +
+ [#188] HACOM Sinh nhật 22 tuổi cùng Acer +
+
+

URL

+ +
+
+ Cập nhật: 29-12-2023, 9:56 am + + 0 + + +
1 +
+ [#188] HACOM Sinh nhật 22 tuổi cùng Acer +
+
+

URL

+ +
+
+ Cập nhật: 29-12-2023, 9:56 am + + 0 + + +
+ + + +
+
\ No newline at end of file diff --git a/old/template/payinstall/add_company.html b/old/template/payinstall/add_company.html new file mode 100644 index 0000000..539eda9 --- /dev/null +++ b/old/template/payinstall/add_company.html @@ -0,0 +1,50 @@ +
+ + + +
+
+
+

Cập nhật công ty

+

HD Saigon

+
+
+
+
+ +
+
+
+ Thông tin cơ bản + +
+ + +
+ +
+ + +
+ +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/old/template/payinstall/add_period.html b/old/template/payinstall/add_period.html new file mode 100644 index 0000000..cc6cef3 --- /dev/null +++ b/old/template/payinstall/add_period.html @@ -0,0 +1,68 @@ +
+ + + +
+
+
+

Cập nhật thời gian

+ + + + Thêm thời gian + +
+

6 tháng

+
+
+
+ +
+
+
+ Thông tin cơ bản + +
+
+ +

Tháng (* ghi số, ví dụ 6)

+
+ +
+ +
+
+ +

% (* ghi số thập phân, ví dụ: 2.6 - tương đương 2.6%)

+
+ +
+ +
+ + +
+ +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/old/template/payinstall/add_upfront.html b/old/template/payinstall/add_upfront.html new file mode 100644 index 0000000..d57eac3 --- /dev/null +++ b/old/template/payinstall/add_upfront.html @@ -0,0 +1,68 @@ +
+ + + +
+
+
+

Cập nhật % trả trước

+ + + + Thêm trả trước + +
+

HD Saigon

+
+
+
+ +
+
+
+ Thông tin cơ bản + +
+
+ +

(* dùng để hiển thị trên web, ví dụ ghi 20 %)

+
+ +
+ +
+
+ +

% (* ghi số, ví dụ: 20% thì ghi là 20)

+
+ +
+ +
+ + +
+ +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/old/template/payinstall/category.html b/old/template/payinstall/category.html new file mode 100644 index 0000000..2e559f5 --- /dev/null +++ b/old/template/payinstall/category.html @@ -0,0 +1,58 @@ +
+
+

Danh mục sản phẩm có trả góp

+ + + + + + + + + + + + + + + + + + +
Danh mụcSPLựa chọn
+

+ 1. Màn Hình Máy Tính +

+
+ + + +
+ +
+
\ No newline at end of file diff --git a/old/template/payinstall/home.html b/old/template/payinstall/home.html new file mode 100644 index 0000000..c335163 --- /dev/null +++ b/old/template/payinstall/home.html @@ -0,0 +1,280 @@ +
+
+

Cài đặt trả góp

+ + + + Thêm công ty + + + + + + + + + + + + + + + + + + + + + + +
STTCông tyThông tin trả góp
1 +

HD Saigon

+
+ + +
+
+
+
+
+ Trả trước + + + Thêm trả trước +
+
+ + + + + + + + + + + + +
20%
20%
+
+ + +
+
+ + + + + + + + + + + + +
20%
20%
+
+ + +
+
+
+
+
+
+ Thời hẹn trả góp + + + Thêm thời gian +
+
+ + + + + + + + + + + + +
20%
20%
+
+ + +
+
+ + + + + + + + + + + + +
20%
20%
+
+ + +
+
+
+
+
+
1 +

HD Saigon

+
+ + +
+
+
+
+
+ Trả trước + + + Thêm trả trước +
+
+ + + + + + + + + + + + +
20%
20%
+
+ + +
+
+ + + + + + + + + + + + +
20%
20%
+
+ + +
+
+
+
+
+
+ Thời hẹn trả góp + + + Thêm thời gian +
+
+ + + + + + + + + + + + +
20%
20%
+
+ + +
+
+ + + + + + + + + + + + +
20%
20%
+
+ + +
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/old/template/payinstall/order.html b/old/template/payinstall/order.html new file mode 100644 index 0000000..319da2b --- /dev/null +++ b/old/template/payinstall/order.html @@ -0,0 +1,83 @@ +
+
+

Danh sách đơn đăng ký

+ +
+ + +
+ +

Tổng số: 16

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STTMã đơnNhà cung cấpSản phẩmĐiều khoảnKhách hàngThời gianLựa chọn
15yl0uuds52fOhdsaigon +
+ +

- SKU: GPSO037

+

- Giá: 1.599.000

+
+
+ +

- SKU: GPSO037

+

- Giá: 1.599.000

+
+
+

- Giá trả góp: 1690146

+

- Trả trước: 799500

+

- Lãi suất: 1.90

+

- Tháng: 6

+
+

- Tên: phạm thanh chiến

+

- Địa chỉ: nhà 04 ngõ 362 lê viết thuật , Xã Hưng Lộc

+

- Tỉnh: Nghệ An

+

- Email: phamthanhchien1808@gmail.com

+

- Điện thoại: 0968 296 041

+
05-03-2024, 12:58 am +
+ Chi + tiết + +
+
+
+
+
\ No newline at end of file diff --git a/old/template/payinstall/order_detail.html b/old/template/payinstall/order_detail.html new file mode 100644 index 0000000..a927919 --- /dev/null +++ b/old/template/payinstall/order_detail.html @@ -0,0 +1,91 @@ +
+ + + +
+
+

Chi tiết đơn hàng trả góp

+

9dDMHsx5CBgG

+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Mã đơn9dDMHsx5CBgG
Nhà cung cấpalepay
Sản phẩm +
+ +

- SKU: GPSO037

+

- Giá: 1.599.000

+
+
+ +

- SKU: GPSO037

+

- Giá: 1.599.000

+
+
Điều khoản +

- Giá trả góp: 6520000

+

- Trả trước: 2500000

+

- Lãi suất: 0

+

- Tháng: 6

+
Khách hàng +

- Nghề nghiệp:

+

- Tên: TRAN QUI

+

- Địa chỉ: 39 Cao Lỗ, Phường 04

+

- Tỉnh: TP HCM

+

- Email: quitnp83@gmail.com

+

- Điện thoại: 0919843649

+

- Ngày sinh:

+

- Giới tính:

+

- CMND:

+

- Siêu thị duyệt hồ sơ:

+
Thời gian05-03-2024, 12:11 am
+
+
+
+
+
\ No newline at end of file diff --git a/old/template/pcbuilder/config_edit.html b/old/template/pcbuilder/config_edit.html new file mode 100644 index 0000000..e9dee50 --- /dev/null +++ b/old/template/pcbuilder/config_edit.html @@ -0,0 +1,73 @@ +
+ + + +
+
+
+

Chỉnh sửa cấu hình và quan hệ lệ thuộc

+

Hướng dẫn: Nhập từng dòng, mỗi dòng là 1 loại linh kiện theo + form mẫu sau:

+ ID danh mục | Tên gọi linh kiện | ID liên quan 1, ID liên quan 2, ID liên quan + 2 +
+
+
+
+ +
+
+
+

+ Ví dụ 1: Ví dụ 1: 30 | Bo mạch chủ | 61,36 +

+

(Trong đó: 30 là ID danh mục của Bo mạch + chủ, 61 là ID danh mục RAM, 36 là ID danh mục + CPU. Bo mạch chủ cần tương thích với RAM và CPU nên chọn ở đây) +

+

+ Ví dụ 2: 10 | Màn hình | +

+

(Trong đó: 10 là ID danh mục + của Màn hình máy tính. Và Màn máy tính không cần phụ thuộc với các linh kiện khác nên để + trống)

+ + + +
+
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/old/template/pcbuilder/home.html b/old/template/pcbuilder/home.html new file mode 100644 index 0000000..9977e23 --- /dev/null +++ b/old/template/pcbuilder/home.html @@ -0,0 +1,70 @@ +
+
+

Danh sách linh kiện

+ + Chỉnh sửa linh kiện + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTID danh mụcTên danh mụcDanh mục liên quanSố lượng Sản phẩm
12Bộ vi xử lý +
+
+ Mainboard - Bo Mạch Chủ + Tạo + quan hệ +
+
+ ,RAM - Bộ Nhớ Trong + Tạo + quan hệ +
+
+
+ 32 +
12Bộ vi xử lý +
+
+ Mainboard - Bo Mạch Chủ + Tạo + quan hệ +
+
+ ,RAM - Bộ Nhớ Trong + Tạo + quan hệ +
+
+
+ 32 +
+
+
\ No newline at end of file diff --git a/old/template/pcbuilder/part_relation.html b/old/template/pcbuilder/part_relation.html new file mode 100644 index 0000000..18e041c --- /dev/null +++ b/old/template/pcbuilder/part_relation.html @@ -0,0 +1,138 @@ +
+ + + +
+
+
+

Mối quan hệ của linh kiện

+

Chọn 1 Sản phẩm ở một danh mục để chọn các Sản phẩm ở liên quan ở danh mục + kia. +

+
+
+
+ +
+
+ CPU - Bộ Vi Xử Lý (#2) +
+ + +
+ +
+

Bộ lọc

+ + + + + + Lọc + sản phẩm +
+ +
+ Kết quả chọn: +

0 sản phẩm

+
+ + + + + + + + + + + + + + + + + + +
+ + STTẢnhSản phẩm (Tổng số: 132)
+ + 1 + + + Máy đo độ bóng Horiba IG-320 (gloss meter),góc đo 60 °, + khoảng đo 0.0-100.0, phân độ 0.1, bộ nhớ 99 phép đo +

+ Góc đo tiêu chuẩn 60 ° Khoảng đo 0-100 khoảng hiển thị 0-200 Kết quả đo chính + xác với độ lặp lại +/- 0,5 % trên toàn dải + đo Quá trình đo cho kết quả không bị ảnh hưởng bởi ánh sáng môi trường bên ngoài + Lấy trung bình đến 99 điểm dữ liệu Xem + các dữ liệu được lưu trữ trên màn hình hiển thị Vận hành thiết bị dễ dàng với 1 + nốt bấm +

+

+ Mã kho: + MOVI213 +

+

Giá: 5.000.000

+

SL: 1

+

Trạng thái: Đang hiển thị

+
+ +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + + + +
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/old/template/product/addon.html b/old/template/product/addon.html new file mode 100644 index 0000000..e94f1a8 --- /dev/null +++ b/old/template/product/addon.html @@ -0,0 +1,57 @@ +
+

Danh sách sản phẩm/dịch vụ bán kèm sản phẩm

+ + + +
+ + + + + + + + + + + + + + + + + + + +
STTThông tinGiáSản phẩmCập nhật
1 + +

Ghi chú: KM màn hình 27 1

+

Tạo lúc: 04-06-2022, 8:28 am, cập nhật: 04-06-2022, 8:28 am

+
3.849.000 vndXem + SP +
+ + + + +
+
+
+
\ No newline at end of file diff --git a/old/template/product/addon_add.html b/old/template/product/addon_add.html new file mode 100644 index 0000000..6048bb4 --- /dev/null +++ b/old/template/product/addon_add.html @@ -0,0 +1,93 @@ +
+ + + +
+
+
+

Cập nhật SP/ Dịch vụ bán kèm

+ + + + Thêm dịch vụ mới + +
+
+ +
+
+ +
+
+
+ Thông tin sản phẩm + +
+ + +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ Hiển thị + + +
+ +
+
+ + +
+
+
+ +
+
\ No newline at end of file diff --git a/old/template/product/attribute.html b/old/template/product/attribute.html new file mode 100644 index 0000000..ef44460 --- /dev/null +++ b/old/template/product/attribute.html @@ -0,0 +1,95 @@ +
+

Danh sách thuộc tính

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTTên hiển thịGiá trịDanh mục đang cóThứ tựTrạng tháiCập nhật
1phong-cachPhong cách5 + + + + + Đang hoạt động + +
+ + + +
+
1phong-cachPhong cách5 + + + + + Đang hoạt động + +
+ + + +
+
+
+ +
\ No newline at end of file diff --git a/old/template/product/attribute_add.html b/old/template/product/attribute_add.html new file mode 100644 index 0000000..6387008 --- /dev/null +++ b/old/template/product/attribute_add.html @@ -0,0 +1,212 @@ +
+ + + +
+
+
+

Cập nhật thuộc tính sản phẩm

+ + + + Thêm thuộc tính mới + +
+
+ +
+
+ +
+
+
+ Thông tin cơ bản + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ Lựa chọn áp dụng +
+
+ + Dùng là tiêu đề nhóm cho các thuộc tính đứng + sau +
+
+ + Dùng lọc Sản phẩm ở danh mục +
+
+ + Hiển thị ở thông tin tóm tắt Sản phẩm +
+
+ + Hiển thị ở bảng thông số kỹ thuật +
+
+ + Dùng để tạo các cấu hình của Sản phẩm +
+
+
+ +
+ Phân loại +
+ + + +
+
+ +
+ Thông tin cơ bản + + + + + + + + + + + + + + + + + + + + + +
STTIDGiá trịMô tảThứ tự hiển thịLựa chọn
14939Nam + + +
+ + +
+
+
+ +
+
+ Thêm giá trị mới + (**) Dùng để sắp xếp Sản phẩm theo thuộc tính khi cần (v.d. theo + dung + lượng ổ cứng laptop giảm + dần) + +
+
+ + +
+
+ + +
+
+ + +
+ Thêm + mới +
+ +
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/old/template/product/brand.html b/old/template/product/brand.html new file mode 100644 index 0000000..fe2b67f --- /dev/null +++ b/old/template/product/brand.html @@ -0,0 +1,99 @@ +
+

Danh sách thương hiệu

+ +
+
+ Tìm theo chữ cái: + A (13) | + B (2) | + C (5) | + D (3) | + E (3) | + F (1) | + G (4) | + H (4) | + I (5) | + J (2) | + K (2) | + L (4) | + M (3) | + N (4) | + O (1) | + P (5) | + Q (1) | + S (8) | + T (5) | + V (2) | + W (2) | + X (1) | + Z (2) | +
+ + + Thêm thương hiệu mới + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STTIDThương hiệuLượt xemLogoSố Sản phẩmCài đặtCập nhật
1750 + 10MOONS + +

Cập nhật cuối: 0000-00-00 00:00:00

+
+ 0 + + + + + +
+

1

+ Xem + SP +
+
+
+ STT + +
+
+
+ + + + +
+
+
+
\ No newline at end of file diff --git a/old/template/product/brand_add.html b/old/template/product/brand_add.html new file mode 100644 index 0000000..4a0b029 --- /dev/null +++ b/old/template/product/brand_add.html @@ -0,0 +1,148 @@ +
+ + + +
+
+
+

Cập nhật Thương hiệu

+ + + + Thêm thương hiệu mới + +
+
+ +
+
+ +
+
+
+ Thông tin cơ bản + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+
+ Upload file +
+ No file chosen +
+
+
+
+ +
+
+ +

Chưa có thư viện ảnh cho phần miêu tả

+
+ +
+ +
+ +
+ Dùng cho SEO +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
\ No newline at end of file diff --git a/old/template/product/category.html b/old/template/product/category.html new file mode 100644 index 0000000..56dd141 --- /dev/null +++ b/old/template/product/category.html @@ -0,0 +1,59 @@ +
+

Danh sách sản phẩm

+ + +
+ + + + + + + + + + + + + + {{page.category_list}} + +
Danh mụcLink webIDXemSPSTTHiển thị nội dungThuộc tínhCập nhật
+
+ +

+ Lưu ý: Tổng sản phẩm ở danh mục mẹ được tính là tổng sản phẩm sản phẩm + của các danh mục con và danh mục mẹ. Bộ đếm không loại trừ sản phẩm trùng nhau (v.d. trường hợp 1 sản phẩm + thuộc cả 2 danh mục con thì sẽ được đếm 2 lần) +

+ + + + +
+ + + \ No newline at end of file diff --git a/old/template/product/category_form.html b/old/template/product/category_form.html new file mode 100644 index 0000000..9799ae6 --- /dev/null +++ b/old/template/product/category_form.html @@ -0,0 +1,294 @@ +
+ + + +
+
+
+

Cập nhật danh mục

+ + + + Thêm danh mục mới + +
+
+ + + {% if page.update_status == 'success' %} +

Cập nhật thành công

+ {% endif %} + +
+
+ +
+
+
+ + + +
+ Thông tin cơ bản + +
+ + +
+ +
+ + +
+
+ +
+ +

Chưa có thư viện ảnh cho phần miêu tả

+
+ +
+ +
+
+ Nhập nội dung cố định (HTML) +
+ +
+

Chưa có thư viện ảnh cho phần miêu tả

+
+ +
+ +
+ Ảnh icon +
+ +
+
+ Upload file +
+ No file chosen +
+
+
+ +
+ Ảnh đại diện +
+ +
+
+ Upload file +
+ No file chosen +
+
+
+
+ +
+ Thông tin danh mục +
+ + +
+

ví dụ: 300000;800000;1500000 có nghĩa là tạo ra 4 khoảng giá cho khách hàng lọc Sản + phẩm, đó là:

+

- Dưới 300000,

+

- Từ 300000 đến 800000,

+

- Từ 800000 đến 1500000

+

- Trên 1500000

+
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ Dùng cho SEO +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ Thay đổi Link truy cập khi thay đổi Tên index? + + Tích chọn để hiển thị hoặc dừng hiển thị +
+
+ +
+ +
+ +
+
+
+ + +
+ +
\ No newline at end of file diff --git a/old/template/product/collection.html b/old/template/product/collection.html new file mode 100644 index 0000000..0aafd44 --- /dev/null +++ b/old/template/product/collection.html @@ -0,0 +1,80 @@ +
+

Bộ sưu tập

+ +
+
+
+ + +
+
+
+ +
+ Hướng dẫn: Bộ sưu tập Sản phẩm bao gồm các Sản phẩm cùng chia sẻ một số đặc điểm nhất định, giúp cho + việc + quảng bá và + định hướng khách hàng hơn. Các bộ sưu tập thường được tạo là: Sản phẩm cho sinh nhật, Sản phẩm cho trẻ mới + sinh, Sản + phẩm công công nghệ mới nhất, Đồ gia dụng nên mua, Khuyến mại giảm giá tháng 10, Chúng tôi đề xuất với bạn, + Top 10 phụ + kiện nên có v..v. (mỗi bộ sưu tập sẽ bao gồm các Sản phẩm từ nhiều danh mục Sản phẩm khác nhau) +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + +
STTIDTên gọi/miêu tảSản phẩmLượt xemCập nhật
126 + Flash Sale | Giảm Giá Cực Sốc ! +
+

Link

+ +
+
+
+

7 sp

+ Xem + danh sách +
+
+ 638 + +
+ Xem + + + + +
+
+
+ +
\ No newline at end of file diff --git a/old/template/product/collection_add.html b/old/template/product/collection_add.html new file mode 100644 index 0000000..ed0b43e --- /dev/null +++ b/old/template/product/collection_add.html @@ -0,0 +1,108 @@ +
+ +
+
+
+

Cập nhật bộ sưu tập Sản phẩm

+ +
+

Flash Sale | Giảm Giá Cực Sốc !

+
+ +
+
+ +
+
+
+ Thông tin cơ bản + +
+

Ảnh icon

+ + + Upload ảnh + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ Dùng cho SEO +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ +
+
\ No newline at end of file diff --git a/old/template/product/config_group.html b/old/template/product/config_group.html new file mode 100644 index 0000000..989e34b --- /dev/null +++ b/old/template/product/config_group.html @@ -0,0 +1,49 @@ +
+

Nhóm cấu hình sản phẩm

+ +
+
+ + Tìm kiếm +
+
+ + + +
+ + + + + + + + + + + + + + + +
STTThông tinCập nhật
1 + Máy In Canon Pixma IX6870-In Phun Mầu +

+ Tạo lúc: 28-12-2023, 11:34 am, + cập nhật: 28-12-2023, 11:34 am +

+
+
+ + +
+
+
+
\ No newline at end of file diff --git a/old/template/product/config_group_attribute.html b/old/template/product/config_group_attribute.html new file mode 100644 index 0000000..365b1fa --- /dev/null +++ b/old/template/product/config_group_attribute.html @@ -0,0 +1,141 @@ +
+ + + +
+
+
+

Cập nhật cấu hình sản phẩm

+ +
+
+ +
+ +
+ +
+ Thuộc tính +
+
+ + +
+
+ + +
+
+
+ +
+ Thuộc tính + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + Thêm giá trị + +
+ +
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/old/template/product/config_group_form.html b/old/template/product/config_group_form.html new file mode 100644 index 0000000..6d7f18d --- /dev/null +++ b/old/template/product/config_group_form.html @@ -0,0 +1,72 @@ +
+ + + +
+
+
+

Cập nhật cấu hình sản phẩm

+ +
+
+ +
+ +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/old/template/product/config_group_product.html b/old/template/product/config_group_product.html new file mode 100644 index 0000000..13f15ad --- /dev/null +++ b/old/template/product/config_group_product.html @@ -0,0 +1,89 @@ +
+ + + +
+
+
+

Cập nhật cấu hình sản phẩm

+ +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + +
STTSản phẩmThuộc tínhCông cụ
1 + Tai nghe Bluetooth True Wireless WIWU TWS12 Màu Hồng + +
+

Màu sắc

+ +
+
+ +
+ +
+
+
+
+
\ No newline at end of file diff --git a/old/template/product/form.html b/old/template/product/form.html new file mode 100644 index 0000000..41106aa --- /dev/null +++ b/old/template/product/form.html @@ -0,0 +1,109 @@ + + +
+ +
+
+
+

Cập nhật #{{ page.product_info.id }}

+
+ +
+ {{ page.product_info.title }} +
+ +
+
+ +
+
+ + {% if page.view_part == "store" %} + {% include 'product/form_components/store' %} + + {% elsif page.view_part == "category" %} + {% include 'product/form_components/category' %} + + {% elsif page.view_part == "seo" %} + {% include 'product/form_components/seo' %} + + {% elsif page.view_part == "description" %} + {% include 'product/form_components/description' %} + + {% elsif page.view_part == "spec-group" %} + {% include 'product/form_components/spec_group' %} + + {% elsif page.view_part == "instruction" %} + {% include 'product/form_components/instruction' %} + + {% elsif page.view_part == "image" %} + {% include 'product/form_components/image' %} + + {% elsif page.view_part == "variant" %} + {% include 'product/form_components/variant' %} + + {% elsif page.view_part == "accessory" %} + {% include 'product/form_components/accessory' %} + + {% elsif page.view_part == "addon" %} + {% include 'product/form_components/addon' %} + + {% elsif page.view_part == "video" %} + {% include 'product/form_components/video' %} + + {% elsif page.view_part == "tag" %} + {% include 'product/form_components/tag' %} + + {% elsif page.view_part == "relation" %} + {% include 'product/form_components/relation' %} + + {% elsif page.view_part == "customer-group" %} + {% include 'product/form_components/customer_group' %} + + {% elsif page.view_part == "component" %} + {% include 'product/form_components/component' %} + + {% elsif page.view_part == "configurable" %} + {% include 'product/form_components/configurable' %} + + {% elsif page.view_part == "compatible" %} + {% include 'product/form_components/compatible' %} + + {% elsif page.view_part == "similar" %} + {% include 'product/form_components/similar' %} + + {% elsif page.view_part == "combo-set" %} + {% include 'product/form_components/combo_set' %} + + {% else %} + + {% include 'product/form_components/basic' %} + {% endif %} + +
+
+
\ No newline at end of file diff --git a/old/template/product/form_components/accessory.html b/old/template/product/form_components/accessory.html new file mode 100644 index 0000000..ccaf79d --- /dev/null +++ b/old/template/product/form_components/accessory.html @@ -0,0 +1,123 @@ + +
+ + +
+ Các danh mục + + +
+ PC, Workstation +
+ + + + + + + + + + + + + + + + + + + +
STTẢnhSản phẩmGiá hiện tạiQuản lý
1 + + + CPU Intel Xeon E5-2665 2.40 GHz / 20MB / 8 Cores 16 Threads / + Socket 2011 +
+ SKU: HN123567 + / + Số lượng: 1 +
+
+ 24.900.000 VND + + + + +
+
+
+
+ PC, Workstation +
+ + + + + + + + + + + + + + + + + + + +
STTẢnhSản phẩmGiá hiện tạiQuản lý
1 + + + CPU Intel Xeon E5-2665 2.40 GHz / 20MB / 8 Cores 16 Threads / + Socket 2011 +
+ SKU: HN123567 + / + Số lượng: 1 +
+
+ 24.900.000 VND + + + + +
+
+
+
+ +
\ No newline at end of file diff --git a/old/template/product/form_components/addon.html b/old/template/product/form_components/addon.html new file mode 100644 index 0000000..e69de29 diff --git a/old/template/product/form_components/basic.html b/old/template/product/form_components/basic.html new file mode 100644 index 0000000..cc5d38e --- /dev/null +++ b/old/template/product/form_components/basic.html @@ -0,0 +1,302 @@ + +
+
+ Thông tin cơ bản +
+ +
+ +
+ Bạn muốn Google tìm ra Sản phẩm này hoặc người xem hiểu ngay về Sản phẩm ? Hãy viết tên Sản phẩm + một cách cụ thể, đầy đủ + nhất. Thay vì viết Vaio ABC, hãy viết : Máy tính xách tay Sony Vaio ABC, 500GB, 4GB RAM, 2.4GHz, + USA, mới 100% Hoặc thay + vì viết Áo 123, hãy viết : Áo khoác nam Made In VietNam, màu đen, đủ size, mã 123 +
+
+
+ + +
+
+
+ +
+ +
+ Nhiều khách hàng tìm Sản phẩm theo Model, bạn có nhập model của Sản phẩm nếu có. Ví dụ: + Laptop Sony Vaio ABC/12 thì + ABC/12 là model Sản phẩm. +
+
+
+ +
+
+
+ +
+ +
+ Nếu bạn có phần mềm quản lý kho hàng chuyên nghiệp và bạn muốn theo dõi các đơn hàng nhận từ + website của Sản phẩm trong + phần mềm này. Khi đó hãy nhập mã kho hàng của Sản phẩm được cung cấp bởi phần mềm kho hàng + vào đây. +
+
+
+ +
+
+
+ + gram +
+ +
+ Dùng tính phí Shipping cho Sản phẩm (nếu có). Đơn vị gram, nếu Sản phẩm 1,2kg = 1200 (gram). +
+
+
+ + +
+
+
+ + +
+ +
+ +
+ +
+ + +
+
+ + +
+
+
+ Thông tin bán hàng + +
+
+ +
+ (* Nếu dùng USD thì theo chuẩn quốc tế v.d. $134.05). + Tỷ giá: + 1 + + + +
+
+
+ + + / + +
+ +
+ +
+
+
+ +
+ vnd (để tham khảo khi cần) +
+
+ + +
+
+
+ +
+ vnd (để tham khảo khi cần) +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ +
+ + + + + + + +
+
+ +
+ +
+ + +
+
+
+
+ +
+
\ No newline at end of file diff --git a/old/template/product/form_components/category.html b/old/template/product/form_components/category.html new file mode 100644 index 0000000..6b7c9fe --- /dev/null +++ b/old/template/product/form_components/category.html @@ -0,0 +1,43 @@ + +
+ Hiện tại Sản phẩm đang được đặt trong các danh mục sau : + + +

+ Bạn có thể chọn thêm danh mục khác cho Sản phẩm hoặc chỉnh lại. +

+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
\ No newline at end of file diff --git a/old/template/product/form_components/combo_set.html b/old/template/product/form_components/combo_set.html new file mode 100644 index 0000000..e69de29 diff --git a/old/template/product/form_components/combo_set_select.html b/old/template/product/form_components/combo_set_select.html new file mode 100644 index 0000000..e69de29 diff --git a/old/template/product/form_components/compatible.html b/old/template/product/form_components/compatible.html new file mode 100644 index 0000000..e69de29 diff --git a/old/template/product/form_components/component.html b/old/template/product/form_components/component.html new file mode 100644 index 0000000..e69de29 diff --git a/old/template/product/form_components/configurable.html b/old/template/product/form_components/configurable.html new file mode 100644 index 0000000..e69de29 diff --git a/old/template/product/form_components/customer_group.html b/old/template/product/form_components/customer_group.html new file mode 100644 index 0000000..e69de29 diff --git a/old/template/product/form_components/description.html b/old/template/product/form_components/description.html new file mode 100644 index 0000000..20d26be --- /dev/null +++ b/old/template/product/form_components/description.html @@ -0,0 +1,27 @@ + +
+ +
+ Chưa có thư viện ảnh cho phần miêu tả +
+ + + + +
\ No newline at end of file diff --git a/old/template/product/form_components/image.html b/old/template/product/form_components/image.html new file mode 100644 index 0000000..fd37648 --- /dev/null +++ b/old/template/product/form_components/image.html @@ -0,0 +1,187 @@ + +
+
+ Cập nhật ảnh sản phẩm + +
+
+
+ +
+
+
+
+ Hình sản phẩm +

(Ảnh chính)

+
+
+
+

STT

+ +
+
+

ALT

+ +
+
+
+ +
+
+
+
+ +
+
+
+
+ Hình sản phẩm +

(Ảnh chính)

+
+
+
+

STT

+ +
+
+

ALT

+ +
+
+
+ +
+
+
+
+ +
+
+
+
+ Hình sản phẩm +

(Ảnh chính)

+
+
+
+

STT

+ +
+
+

ALT

+ +
+
+
+ +
+
+
+
+ +
+
+
+
+ Hình sản phẩm +

(Ảnh chính)

+
+
+
+

STT

+ +
+
+

ALT

+ +
+
+
+ +
+
+
+
+ +
+
+
+
+ Hình sản phẩm +

(Ảnh chính)

+
+
+
+

STT

+ +
+
+

ALT

+ +
+
+
+ +
+
+
+
+ +
+ Thêm ảnh cho sản phẩm +

Bạn có thể: Chọn ảnh có sẵn trong kho ảnh, hoặc upload ảnh từ máy tính:

+ +
+ + +

Chú ý: Bạn có thể chọn nhiều ảnh cùng 1 + lúc: + Chấp nhận các file ảnh: jpg,gif

+ +
+ +
+ Chú ý: +
    +
  • - Ảnh Sản phẩm kích thước lớn sẽ được tự động co lại thành các ảnh + nhỏ hơn cho những vị trí liên quan. Tùy giao + diện website của bạn mà kích thước ảnh Sản phẩm khác nhau. Click + vào đây để + quy định kích thước ảnh Sản phẩm.
  • +
  • + - Chỉ dùng file ảnh đuôi .jpg và .gif. +
  • +
  • + - Cập nhật hình ảnh cho Sản phẩm ở nhiều góc cạnh, màu sắc để người dùng xem + rõ nhất. Nên cập nhật ảnh có kích thước lớn + và độ phân giải cao. +
  • +
+
+
\ No newline at end of file diff --git a/old/template/product/form_components/instruction.html b/old/template/product/form_components/instruction.html new file mode 100644 index 0000000..e69de29 diff --git a/old/template/product/form_components/price.html b/old/template/product/form_components/price.html new file mode 100644 index 0000000..e69de29 diff --git a/old/template/product/form_components/relation.html b/old/template/product/form_components/relation.html new file mode 100644 index 0000000..42f0c4c --- /dev/null +++ b/old/template/product/form_components/relation.html @@ -0,0 +1,12 @@ +
+
+ Danh sách liên quan + + + + +
+
\ No newline at end of file diff --git a/old/template/product/form_components/seo.html b/old/template/product/form_components/seo.html new file mode 100644 index 0000000..a33f037 --- /dev/null +++ b/old/template/product/form_components/seo.html @@ -0,0 +1,76 @@ + +
+
+
+ + +
+
+ + +
+ +
+ +
+ +
+
+
+
+ + * để trống sẽ dùng link mặc định của hệ thống +
+ +
+ +
+
+
+ + * 0 ký tự , 0 từ +
+ +
+
+
+ + * 0 ký tự , 0 từ +
+ +
+
+ +
+
+ + * 0 ký tự , 0 từ (Khuyến nghị: 160 ký tự) +
+ +
+ +
+ +
+ +
+
\ No newline at end of file diff --git a/old/template/product/form_components/similar.html b/old/template/product/form_components/similar.html new file mode 100644 index 0000000..e69de29 diff --git a/old/template/product/form_components/spec.html b/old/template/product/form_components/spec.html new file mode 100644 index 0000000..e69de29 diff --git a/old/template/product/form_components/spec_group.html b/old/template/product/form_components/spec_group.html new file mode 100644 index 0000000..0eb9976 --- /dev/null +++ b/old/template/product/form_components/spec_group.html @@ -0,0 +1,137 @@ + +
+ +
+ Cập nhật thuộc tính cho Sản phẩm + +
+ +
+ +
+ Dòng cpu +

dong-cpu

+ Dùng là bộ lọc - Dùng tạo lựa chọn SP - Hiển thị ở tóm tắt +
+ + + + + +
+
+
+ Bổ sung giá trị: +

mỗi giá trị 1 dòng

+ +
+ +
+
+ +
+ Dòng cpu +

dong-cpu

+ Dùng là bộ lọc - Dùng tạo lựa chọn SP - Hiển thị ở tóm tắt +
+ + + + + +
+
+
+ Bổ sung giá trị: +

mỗi giá trị 1 dòng

+ +
+ +
+
+
+ Dòng cpu +

dong-cpu

+ Dùng là bộ lọc - Dùng tạo lựa chọn SP - Hiển thị ở tóm tắt +
+ + + + + +
+
+
+ Bổ sung giá trị: +

mỗi giá trị 1 dòng

+ +
+ +
+
+ +
+ + +
\ No newline at end of file diff --git a/old/template/product/form_components/spec_multiple.html b/old/template/product/form_components/spec_multiple.html new file mode 100644 index 0000000..e69de29 diff --git a/old/template/product/form_components/store.html b/old/template/product/form_components/store.html new file mode 100644 index 0000000..e69de29 diff --git a/old/template/product/form_components/tag.html b/old/template/product/form_components/tag.html new file mode 100644 index 0000000..b64ffb0 --- /dev/null +++ b/old/template/product/form_components/tag.html @@ -0,0 +1,39 @@ +
+
+ + +
+ + + + + + + + + + + +
STTTag
1ATO Test + + + +
+
+ +
+
+ Thêm tag nhanh cho sản phẩm +

Nhập Tag cho sản phẩm vào ô dưới đây và nhấn cập nhật. Mỗi Tag 1 dòng:

+ + + +
+
\ No newline at end of file diff --git a/old/template/product/form_components/variant.html b/old/template/product/form_components/variant.html new file mode 100644 index 0000000..2c5b533 --- /dev/null +++ b/old/template/product/form_components/variant.html @@ -0,0 +1,139 @@ + +
+
+
+ Chủng loại của sản phẩm + + + + Sửa thuộc tính + + +
+
+ Chú ý: + Giá bán của sản phẩm sẽ là giá của chủng loại được chọn. Nếu giá chủng loại=0 thì sẽ mặc định dùng + giá sản phẩm. + Cài đặt giá chung cho sản phẩm ở Tab "Cơ bản" + +
+ +
+ Cài nhanh cho tất cả chủng loại + +
+
+ + +
+
+ + +
+
+
+ +
+ + + + + + + + + + + + + + + +
ẢnhChủng loại
+ + + + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+ + + + Thêm chủng loại + +
+
+ + +
+
\ No newline at end of file diff --git a/old/template/product/form_components/video.html b/old/template/product/form_components/video.html new file mode 100644 index 0000000..729e265 --- /dev/null +++ b/old/template/product/form_components/video.html @@ -0,0 +1,42 @@ +
+
+ Youtobe Video +

Bạn cần gắn url của video được cung cấp bởi Youtube vào đây +

+ Ví dụ: https://www.youtube.com/watch?NR=1&v=QqzJhk1p4rU&feature=endscreen +
+ + + + + + + + + + + +
Link youtobeMô tả (nếu có)
+ + + + + + + +
+
+ + + + + Thêm chủng loại + +
+
+ + +
+
\ No newline at end of file diff --git a/old/template/product/home.html b/old/template/product/home.html new file mode 100644 index 0000000..1781418 --- /dev/null +++ b/old/template/product/home.html @@ -0,0 +1,333 @@ +
+

Danh sách sản phẩm ({{ page.total | format_number }})

+
+
+
+ +
+ + Chọn thương hiệu để xem + +
+ +
+
+ + + + + + + + + + + + + {% assign counter = 0 %} + {% for item in page.item_list %} + {% increment counter %} + + + + + + + + + {% endfor %} + + +
STTẢnhSản phẩm (Tổng số: {{page.total}})Thông tin bán hàng + + Công cụ
{{ counter }} + + {{item.image_count}} ảnh + + [#{{item.id}}] + - + {{item.title}} + + {% if item.config_count > 0 %}

[Có cấu hình]

{% endif %} +
+ Danh mục: + Màn hình Máy tính +
+
+ Mã kho + {{item.sku}} + | + Hãng + +
+ + {% assign date_now = "now" | date: format %} + {% assign date_last = item.last_update %} + {% assign time_conlai = date_now | minus: date_last %} + +

Cập nhật: {{item.last_update | date: "Y-m-d h:i" }} ( + {{ time_conlai | divided_by: 86400 | round}} + ngày)

+

Người cập nhật : {{item.last_update_by}}

+
+
+ Giá bán: + + {% if item.price > 0 %} + {{item.price | format_price }} vnd + {% else %}` + {{item.price }} vnd + {% endif %} + +
+
+ Giá thị trường: + + {% if item.market_price > 0 %} {{item.market_price | format_price }} vnd {% else %} + 0 vnd {% endif %} + +
+

Bảo hành: {{item.warranty}}

+

Khuyến mại: {{item.special_offer}}

+
+ SL tổng: + {{item.quantity}} +
+
+ + + + + + + + + + + + +
+
+
+ + + + + + + {% if item.status == 0 %} + + + + {% else %} + + + + {% endif %} + + + +
+
+
+ +
+ {% for paging in page.pagination.collection %} + {% if paging.name == 'next' %} + + + + {% elsif paging.name == 'prev' %} + + + + {% else %} + {{paging.name}} + {% endif %} + {% endfor %} +
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/old/template/product/list_competitor.html b/old/template/product/list_competitor.html new file mode 100644 index 0000000..bf9bebd --- /dev/null +++ b/old/template/product/list_competitor.html @@ -0,0 +1,130 @@ +
+

So sánh giá đối thủ

+ +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTSản phẩm (Tổng số: 53)Cập nhật
1 + Ổ Cứng SSD KINGMAX 120GB SMV32 +

- Giá: 499.000

+

-Mã kho: OCKM0008

+

- Cập nhật: 25-01-2024, 9:46 am (29 ngày)

+
+ Cập + nhật +
1 + Ổ Cứng SSD KINGMAX 120GB SMV32 +

- Giá: 499.000

+

-Mã kho: OCKM0008

+

- Cập nhật: 25-01-2024, 9:46 am (29 ngày)

+
+ Cập + nhật +
1 + Ổ Cứng SSD KINGMAX 120GB SMV32 +

- Giá: 499.000

+

-Mã kho: OCKM0008

+

- Cập nhật: 25-01-2024, 9:46 am (29 ngày)

+
+ Cập + nhật +
1 + Ổ Cứng SSD KINGMAX 120GB SMV32 +

- Giá: 499.000

+

-Mã kho: OCKM0008

+

- Cập nhật: 25-01-2024, 9:46 am (29 ngày)

+
+ Cập + nhật +
+
+
+ + + + + + \ No newline at end of file diff --git a/old/template/product/set.html b/old/template/product/set.html new file mode 100644 index 0000000..8aaf709 --- /dev/null +++ b/old/template/product/set.html @@ -0,0 +1,64 @@ +
+

Danh sách set

+
+
Giới thiệu: "Combo Set" là một "set" (danh sách) các sản phẩm được cài giảm giá + sẵn. + Các + mức giá giảm này + sẽ được áp + dụng khi người mua hàng mua cùng 1 sản phẩm chính. Ví dụ đơn giản: Bạn tạo 1 set (danh sách) các phụ + kiện dành cho + iPhone với các mức chiết khấu đáng kể nếu người dùng mua kèm iPhone. Sau khi tạo set xong, bạn chọn 1 + vài sản phẩm + iPhone có thể áp dựng set này. Như vậy xem website có thể chọn mua iPhone và mua thêm 1 số sản phẩm + trong set mà bạn đã + thiết lập.
+ + + Thêm + set mới +
+
+ + + + + + + + + + + + + + + + + + + +
STTThông tinCập nhậtSản phẩm sử dụng set nàyQuản lý
1(ID: 658) Thẻ Nhớ CAEZ CAIM CAIS +

Thẻ Nhớ cho các dòng CAM: CAEZ CAIM CAIS

+
+

Nguyễn Tiến Dinh

+

01-03-2024, 11:59 am

+
+
+

137 sp

+ Xem + danh sách +
+
+
+ Chi + tiết + + +
+
+
+ +
\ No newline at end of file diff --git a/old/template/product/set_form.html b/old/template/product/set_form.html new file mode 100644 index 0000000..eba4689 --- /dev/null +++ b/old/template/product/set_form.html @@ -0,0 +1,320 @@ +
+ +
+
+
+

Cập nhật combo set

+ + + + Thêm set mới + +
+

Thẻ Nhớ CAEZ CAIM CAIS

+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
Tên set + +
+ Mô tả tóm tắt + + +
+ Trạng thái + + + + +
+ Thời trang hiển thị + +
+

Bắt đầu

+
+ + +
+

Giờ

+ +
+ +
+

Kết thúc

+
+ + +
+

Giờ

+ +
+
+ Nhóm sản phẩm trong Set + + + + + + + + + + +
+ Nhóm sản phẩm + + +

Tên gọi

+ +
+ Sản phẩm + + +
+

Tên gọi

+ [Chọn sản + phẩm] +
+ +

Giá đang bán : 129.000

+
+

Cài giảm

+ + + +
+
+ + + +
+
+ [+] Thêm + nhóm sản phẩm +
+ + + +
+
+
+
+
\ No newline at end of file diff --git a/old/template/product/set_product.html b/old/template/product/set_product.html new file mode 100644 index 0000000..4f2814c --- /dev/null +++ b/old/template/product/set_product.html @@ -0,0 +1,308 @@ +
+ +
+
+

Cập nhật combo set

+

Thẻ Nhớ CAEZ CAIM CAIS

+
+
+
+ +
+
+
+
+

Danh sách sản phẩm sử dụng set

+ + + + Thêm set mới + +
+ +
+
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTẢnhTênLựa chọn
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
+ + + +
+
+
+
+
\ No newline at end of file diff --git a/old/template/product/spec_group.html b/old/template/product/spec_group.html new file mode 100644 index 0000000..a552fcc --- /dev/null +++ b/old/template/product/spec_group.html @@ -0,0 +1,49 @@ +
+

Nhóm thông số kỹ thuật

+ +
+ +
+ +
+

+ Hướng dẫn: + Nhóm thông số kỹ thuật được ghép cho từng sản phẩm cụ thể +

+ + + Thêm nhóm mới + +
+
+ + + + + + + + + + + + + + + +
STTTên gọi/miêu tảCập nhật
1 + Camera + +
+ + +
+
+
+
\ No newline at end of file diff --git a/old/template/product/spec_group_add.html b/old/template/product/spec_group_add.html new file mode 100644 index 0000000..58cc4e4 --- /dev/null +++ b/old/template/product/spec_group_add.html @@ -0,0 +1,78 @@ +
+ + + +
+
+
+

Cập nhật thông số kỹ thuật

+ Thông tin chung +
+
+
+
+ +
+
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + +
+
+ + +
+
+
+ +
+
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/old/template/product/spec_group_info.html b/old/template/product/spec_group_info.html new file mode 100644 index 0000000..92c768d --- /dev/null +++ b/old/template/product/spec_group_info.html @@ -0,0 +1,95 @@ +
+ + + +
+

Thông tin nhóm thông số kỹ thuật

+
+
+ Nhóm thông số +

THIẾT BỊ TRỢ GIẢNG

+
+ + + + Thêm nhóm mới + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTHeaderThông tinDiễn dảiThứ tự hiển thịBắt buộc nhập cho spCập nhật
1Thông tin chungĐặc tính-99 +
+ + +
+
1Thông tin chungĐặc tính-99 +
+ + +
+
1Thông tin chungĐặc tính-99 +
+ + +
+
+
+
\ No newline at end of file diff --git a/old/template/report/customer.html b/old/template/report/customer.html new file mode 100644 index 0000000..412339a --- /dev/null +++ b/old/template/report/customer.html @@ -0,0 +1,122 @@ +
+
+

Thống kê truy cập

+ +
+
+
+
+ 1.45K + Truy cập web trong tuần này +
+
+
+ 12% + +
+
+
+ +
+ +
+ +
+ +
+
+
+ Truy cập website + +
+ +
+ +
+ +
+ +
+
+
+ Truy cập theo tỉnh thành + +
+ +
+ +
+ +
+
+
+ + +
+
+
+
+

Khách hàng đăng ký mới trong 30 ngày:

+ 11.458 +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTNgàySố truy cập
105-12-2021235
105-12-2021235
105-12-2021235
105-12-2021235
105-12-2021235
+ +
+
+ +
+
\ No newline at end of file diff --git a/old/template/report/order.html b/old/template/report/order.html new file mode 100644 index 0000000..afcb472 --- /dev/null +++ b/old/template/report/order.html @@ -0,0 +1,131 @@ +
+
+

Thống kê đơn hàng

+ +
+
+
+
+ 1.45K + Truy cập web trong tuần này +
+
+
+ 12% + +
+
+
+ +
+ +
+ +
+ +
+
+
+ Truy cập website + +
+ +
+ +
+ +
+ +
+
+
+ Truy cập theo tỉnh thành + +
+ +
+ +
+ +
+
+
+ + +
+
+
+
+

Khách hàng đăng ký mới trong 30 ngày:

+ 11.458 +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTNgàySố đơn hàng mớiTình trạng
105-12-2021235 + +
105-12-2021235 + +
105-12-2021235 + +
105-12-2021235 + +
+ +
+
+ +
+
\ No newline at end of file diff --git a/old/template/report/product_buy.html b/old/template/report/product_buy.html new file mode 100644 index 0000000..88b75d4 --- /dev/null +++ b/old/template/report/product_buy.html @@ -0,0 +1,101 @@ +
+
+

Sản phẩm mua nhiều

+ +
+
+ Top sản phẩm mua nhiều + +
+
+ + + +
+
+ +
+
+
+

Khách hàng đăng ký mới trong 30 ngày:

+ 11.458 +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTMã sốSản phẩmSố đơn
1#2081 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 223
1#2081 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 223
1#2081 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 223
1#2081 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 223
+
+ + +
+
\ No newline at end of file diff --git a/old/template/report/product_visit.html b/old/template/report/product_visit.html new file mode 100644 index 0000000..4017c52 --- /dev/null +++ b/old/template/report/product_visit.html @@ -0,0 +1,101 @@ +
+
+

Sản phẩm xem nhiều

+ +
+
+ Top sản phẩm xem nhiều + +
+
+ + + +
+
+ +
+
+
+

Khách hàng đăng ký mới trong 30 ngày:

+ 11.458 +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTMã sốSản phẩmSố đơn
1#2081 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 223
1#2081 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 223
1#2081 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 223
1#2081 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 223
+
+ + +
+
\ No newline at end of file diff --git a/old/template/report/referer.html b/old/template/report/referer.html new file mode 100644 index 0000000..156bdb2 --- /dev/null +++ b/old/template/report/referer.html @@ -0,0 +1,113 @@ +
+
+

Website giới thiệu

+ +
+
+
+
+ 1.45K + Truy cập web trong tuần này +
+
+
+ 12% + +
+
+
+ +
+ +
+ +
+ +
+
+
+ Khách hàng biết tới website + +
+ +
+ +
+ +
+ +
+
+ +
+
+
+

Các website mang người xem đến website của quý khách nhiều nhất trong tháng

+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STTNguồnLượt giới thiệu
1 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 278
1 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 278
1 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 278
+
+ +
+ Giải thích thuật ngữ: +
    +
  • Khi quý khách quảng cáo Sản phẩm, tin tức ở diễn đàn hay ở website nào khác, + hãy cố gắng đặt đường link để + người dùng click vào link đó và truy cập vào website của quý khách. Càng nhiều đường link, website + của + quý khách sẽ được + các bộ máy tìm kiếm như Google, Yahoo, Bing tìm đến và lưu lại thông tin. Như vậy sẽ có hiệu quả tốt + khi + người dùng tìm + kiếm Google và nhìn thấy được website của quý khách mà không phải là tin bài quý khách đã quảng cáo + ở + các website khác. +
  • +
+
+ +
+
\ No newline at end of file diff --git a/old/template/report/search.html b/old/template/report/search.html new file mode 100644 index 0000000..126025f --- /dev/null +++ b/old/template/report/search.html @@ -0,0 +1,95 @@ +
+
+

Thống kê tìm kiếm

+ +
+
+
+
+ 1.45K + Truy cập web trong tuần này +
+
+
+ 12% + +
+
+
+ +
+ +
+ +
+ +
+
+
+ Công cụ tìm kiếm website + +
+ +
+ +
+ +
+ +
+
+ +
+
+
+

Tìm kiếm tại website nhiều nhất trong 30 ngày qua

+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STTNguồnLượt giới thiệu
1 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 278
1 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 278
1 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 278
+
+ +
+
\ No newline at end of file diff --git a/old/template/report/visitor.html b/old/template/report/visitor.html new file mode 100644 index 0000000..1ac4fb9 --- /dev/null +++ b/old/template/report/visitor.html @@ -0,0 +1,198 @@ +
+
+

Thống kê truy cập

+ +
+
+
+
+ 1.45K + Truy cập web trong tuần này +
+
+
+ 12% + +
+
+
+ +
+ +
+ +
+ +
+
+
+ Truy cập website + +
+ +
+ +
+ +
+ +
+
+
+ Truy cập theo tỉnh thành + +
+ +
+ +
+ +
+
+
+ + +
+
+
+
+

Tổng số lượt truy cập trong 30 ngày:

+ 11.458 +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTNgàySố truy cập
105-12-2021235
105-12-2021235
105-12-2021235
105-12-2021235
105-12-2021235
+ +
+
+
+
+

Tổng số lượt truy cập trong 30 ngày:

+ 11.458 +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTNgàySố truy cập
105-12-2021235
105-12-2021235
105-12-2021235
105-12-2021235
105-12-2021235
+ +
+
+ +
+ Giải thích thuật ngữ: +
    +
  • Giới thiệu bởi: + Giới thiệu bởi : Là địa chỉ website mang người xem tới website. Ví dụ: Người xem đang xem website + abcd.com và thấy có + đường link Sản phẩm, người đó click vào đường link để tới website của bạn, như vậy website abcd.com + sẽ được hệ thống lưu + lại. Hãy quảng cáo link Sản phẩm hoặc gian hàng của bạn ở các website khác để tăng sự giới thiệu. +
  • +
  • + IP : Đây là địa chỉ IP của máy tính của người xem. Mỗi máy tính đều có 1 địa chỉ riêng để + nhận + biết (và gọi IP). Trong + một mạng nội bộ chia sẻ (ví dụ: quán Internet Cafe) thì nhiều máy dùng chung 1 đường dây mạng sẽ + có cùng 1 địa chỉ IP + (khi truy cập bên ngoài). +
  • +
  • + Thời gian : Thời gian truy cập website +
  • +
  • + Trang được xem : Trang chi tiết trên website được xem. +
  • +
+
+ +
+
\ No newline at end of file diff --git a/old/template/shipping2/fee.html b/old/template/shipping2/fee.html new file mode 100644 index 0000000..ac76b61 --- /dev/null +++ b/old/template/shipping2/fee.html @@ -0,0 +1,50 @@ +
+ + + +
+ Chọn danh sách tỉnh thành để quản lý phí tới Quận/Huyện + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTTênQuản lý
1Hà nộiQuản lý +
1Hà nộiQuản lý +
1Hà nộiQuản lý +
1Hà nội + Quản lý +
+ +
+ +
\ No newline at end of file diff --git a/old/template/shipping2/home.html b/old/template/shipping2/home.html new file mode 100644 index 0000000..2bb298a --- /dev/null +++ b/old/template/shipping2/home.html @@ -0,0 +1,22 @@ +
+
+

Quản lý shipping

+ +
+

Cài đặt hệ thống chung

+ + + Cài đặt + +
+
+

Cài đặt phí ship từng vùng

+ + + Cài đặt + +
+
+
\ No newline at end of file diff --git a/old/template/shipping2/setting.html b/old/template/shipping2/setting.html new file mode 100644 index 0000000..910a3e2 --- /dev/null +++ b/old/template/shipping2/setting.html @@ -0,0 +1,91 @@ +
+ + + +
+
+ Cài đặt giá trị đơn hàng: +
+

Giá trị đơn hàng không tính giá : +

+ +
+
(* Đơn hàng có giá trị nhỏ hơn giá trị này sẽ được áp dụng biểu + phí theo + vùng)
+ + Cài đặt phí thu hộ: + + + + + + + + + + + + + + + + + + + + + + +
STTGiá trị đơn hàng dưới mứcPhú thu hộ (VNĐ)Quản lý
1 + + + + + +
1 + + + + + +
+ + [+] Thêm hàng + + + +
+ Hướng dẫn cài phí thu hộ +

Phí thu hộ được tính theo giá trị cần thu hộ, ví dụ:

+
    +
  • - Từ 0 đến <=1 triệu=30.000đ=> Cần nhập là 1.000.000
  • +
  • - Từ >1 triệu đến <= 2 triệu=50.000đ=> Cần nhập là 2.000.000
  • +
  • - Từ >2 triệu đến <= 3 triệu=70.000đ=> Cần nhập là 3.000.000
  • +
  • - Từ >3 triệu đến <= 4 triệu=90.000đ=> Cần nhập là 4.000.000
  • +
  • - Từ >4 triệu đến <= 5 triệu=110.000đ=> Cần nhập là 5.000.000
  • +
  • - Từ >5 triệu đến <= 6 triệu=130.000đ=> Cần nhập là 6.000.000
  • +
  • - Từ >6 triệu đến <= 7 triệu=150.000đ=> Cần nhập là 7.000.000
  • +
  • - Từ >7 triệu đến <= 8 triệu=170.000đ=> Cần nhập là 8.000.000
  • +
  • - Từ >8 triệu đến <= 9 triệu=190.000đ=> Cần nhập là 9.000.000
  • +
  • - Trên 9 triệu = 0đ => Cần nhập là 10.000.000.000 (nhập số lớn nhất có thể)
  • +
+

Để xóa phí nào nó, click "Xóa bỏ" và nhấn "Cập nhật" để lưu lại

+
+ +
+
+
\ No newline at end of file diff --git a/old/template/system/ban_ip.html b/old/template/system/ban_ip.html new file mode 100644 index 0000000..8b189a2 --- /dev/null +++ b/old/template/system/ban_ip.html @@ -0,0 +1,72 @@ + + +
+
+

Chặn IP xấu truy cập website

+

Chức năng này cho phép bạn ngăn chặn một số địa chỉ IP truy cập vào + website. Thường dùng + để chặn đối thủ hoặc + những + người có mục đích xấu muốn phá hoại website của bạn

+
+ + + +
+
+ Nhập IP cần cấm (mỗi IP 1 dòng) +
+ Bạn có thể xem địa chỉ IP tại Báo cáo + truy + cập. Để loại bỏ + IP khỏi danh sách cấm, xóa IP + trong danh sách + và cập nhật + lại. +
+ +
+ +
+

Hỗ trợ các định dạng: 127.0.0.1

+

172.0.0.*

+

173.0.*.*

+

126.1.0.0/24

+

125.0.0.1-125.0.0.9

+
+ +
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/old/template/system/change_info.html b/old/template/system/change_info.html new file mode 100644 index 0000000..628a1d2 --- /dev/null +++ b/old/template/system/change_info.html @@ -0,0 +1,112 @@ + + +
+
+

Thay đổi nội dung doanh nghiệp

+
+ + + +
+
+
+
+ + * nếu để trống sẽ dùng tên gọi website +
+ +
+
+ + +
+
+ + +
+
+
+ +
+ +
+ + 1 + 2 + 3 + 4 + +
+
+
+ Thông tin giới thiệu chi tiết + +
+
+ Thông tin liên hệ + +
+
+
+ +
+
+
\ No newline at end of file diff --git a/old/template/system/email.html b/old/template/system/email.html new file mode 100644 index 0000000..a912a39 --- /dev/null +++ b/old/template/system/email.html @@ -0,0 +1,394 @@ + + +
+
+

Cài đặt Email

+
+ + + +
+
+ Cài đặt email cho website + Cài đặt email + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTPhân loạiCài đặtHành động
1 + Email gửi thông báo cho khách hàng (*) +

Dùng gửi khách hàng khi họ đặt hàng, báo tình trạng đơn hàng v.v. Cần phải dùng một dịch + vụ gửi email để gửi đi. Mặc + định là dùng hotro@demopc8.hurasoft.com và sử dụng dịch vụ gửi email của HuraSoft. +

+
+
+

Email *

+ +
+
+ Cập + nhật +
2 + Email nhận chung (*) +

Bắt buộc. Sẽ được dùng để nhận tất cả các thông báo của website nếu các email khác dưới + đây không được cài. +

+
+
+

Email *

+ +
+
+ Cập + nhật +
3 + Email nhận thông báo đơn hàng +

Các đơn hàng mới sẽ được gửi thông báo vào Email này

+
+
+

Email *

+ +
+
+ Cập + nhật +
4 + Email nhận liên hệ +

Các liên hệ mới sẽ được gửi thông báo vào Email này

+
+
+

Email *

+ +
+
+ Cập + nhật +
+
+
+ Cài đặt email cho website +
+ Hướng dẫn: Nên dùng Editor ở phần sửa template để chỉnh + nội dung html rồi copy/cập nhật ở + đây +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTPhân loạiNội dungHành động
1 + Email gửi thông báo đơn hàng mới cho khách hàng
email_tpl_order_detail +
+ Xem + nội + dung + + + Cập + nhật +
2 + Email gửi thông báo cho khách hàng khi đơn hàng được cập nhật trạng thái + mới
email_tpl_update_buyer_order_status +
+ Xem + nội + dung + + + Cập + nhật +
3 + Email gửi thông báo đơn hàng mới cho khách hàng
email_tpl_order_detail +
+ Xem + nội + dung + + + Cập + nhật +
4 + Email gửi thông báo đơn hàng mới cho khách hàng
email_tpl_order_detail +
+ Xem + nội + dung + + + Cập + nhật +
5 + Email gửi thông báo đơn hàng mới cho khách hàng
email_tpl_order_detail +
+ Xem + nội + dung + + + Cập + nhật +
6 + Email gửi thông báo đơn hàng mới cho khách hàng
email_tpl_order_detail +
+ Xem + nội + dung + + + Cập + nhật +
7 + Email gửi thông báo đơn hàng mới cho khách hàng
email_tpl_order_detail +
+ Xem + nội + dung + + + Cập + nhật +
8 + Email gửi thông báo đơn hàng mới cho khách hàng
email_tpl_order_detail +
+ Xem + nội + dung + + + Cập + nhật +
9 + Email gửi thông báo đơn hàng mới cho khách hàng
email_tpl_order_detail +
+ Xem + nội + dung + + + Cập + nhật +
+ +
+
+
\ No newline at end of file diff --git a/old/template/system/form_setup/general.html b/old/template/system/form_setup/general.html new file mode 100644 index 0000000..0c4c19e --- /dev/null +++ b/old/template/system/form_setup/general.html @@ -0,0 +1,98 @@ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Tạm đóng website +

(Nếu bạn chưa muốn người dùng xem website, hãy tạm đóng lại)

+
+
+ Mật khẩu để xem + + (Xóa bỏ mật khẩu để mở lại website) +
+
+ Nội dung thông báo khách hàng: + +
+
+ Xác thực tên miền với Google Sử dụng khi cài email của Google + + +

Nhập nội dung của file Google vào ô trên

+
+ (

V.d.

google-site-verification: + google0b0cdca26a03ce2e.html) +
+
Tỷ giá ngoại tệ (USD/VND) + +
+ Số lượng sản phẩm / 1 trang danh mục + + +
+ Thứ tự sản phẩm mặc định trong danh mục + + +
+ Số lượng tin tức hiển thị / 1 trang + + +
+ SiteManager (nếu có) + + + v.d. http://web.hurasoft.com +
+ SiteManager Access Key + + + xem trong sitemanager +
+ + +
+
\ No newline at end of file diff --git a/old/template/system/form_setup/homepage.html b/old/template/system/form_setup/homepage.html new file mode 100644 index 0000000..b92d1e4 --- /dev/null +++ b/old/template/system/form_setup/homepage.html @@ -0,0 +1,121 @@ +
+
+ Hướng dẫn: +
    +
  • - ID danh mục: nhập danh sách các ID danh mục cách nhau dấu , hoặc khoảng trắng. + Để percat nếu muốn hệ thống + tự động lấy theo danh mục sản phẩm đang xem
  • +
  • - Số lượng hiển thị : Nhập 0 nếu không muốn dùng nội dung, điền số > 0 để có + số lượng cần lấy Để website chạy nhanh nhất, chỉ kích hoạt các nội dung mà giao diện hiển thị cần đến. +
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Sản phẩm nổi bật (HOT) + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Sản phẩm bán chạy nhất (BEST SALE) + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Sản phẩm mới nhất (NEW) + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Sản phẩm xả hàng (SALE OFF) + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Tin tức + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Bộ sưu tập sản phẩm + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ + +
+
\ No newline at end of file diff --git a/old/template/system/form_setup/pro_category.html b/old/template/system/form_setup/pro_category.html new file mode 100644 index 0000000..6a52fb4 --- /dev/null +++ b/old/template/system/form_setup/pro_category.html @@ -0,0 +1,121 @@ +
+
+ Hướng dẫn: +
    +
  • - ID danh mục: nhập danh sách các ID danh mục cách nhau dấu , hoặc khoảng trắng. + Để percat nếu muốn hệ thống + tự động lấy theo danh mục sản phẩm đang xem
  • +
  • - Số lượng hiển thị : Nhập 0 nếu không muốn dùng nội dung, điền số > 0 để có + số lượng cần lấy Để website chạy nhanh nhất, chỉ kích hoạt các nội dung mà giao diện hiển thị cần đến. +
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Sản phẩm nổi bật (HOT) + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Sản phẩm bán chạy nhất (BEST SALE) + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Sản phẩm mới nhất (NEW) + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Sản phẩm xả hàng (SALE OFF) + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Tin tức + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Bộ sưu tập sản phẩm + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ + +
+
\ No newline at end of file diff --git a/old/template/system/form_setup/product.html b/old/template/system/form_setup/product.html new file mode 100644 index 0000000..c5c3945 --- /dev/null +++ b/old/template/system/form_setup/product.html @@ -0,0 +1,71 @@ +
+
+ + + + + + + + + + + + + + +
+ Kích thước ảnh sản phẩm + Điều chỉnh kích thước ảnh cho phù hợp với giao diện + website: + +
+ thumb + + px +
+
+ small + + px +
+
+ medium + + px +
+
+ large + + px +
+
+ Tạo ảnh vuông + +
+ + Tạo các ảnh nhỏ hình vuông với kích thước như trên + +
+
+ Ghi chữ bản quyền lên ảnh + +
+ + Ghi bản quyền +
+

(Tên miền website demopc8.hurasoft.com sẽ được ghi lên + ảnh sản + phẩm)

+
+ + +
+
\ No newline at end of file diff --git a/old/template/system/form_store_design/background.html b/old/template/system/form_store_design/background.html new file mode 100644 index 0000000..defcb14 --- /dev/null +++ b/old/template/system/form_store_design/background.html @@ -0,0 +1,21 @@ +
+ + + + + +
Thay Ảnh nền +
+ +
+ + + + Bạn có thể thay nền website bằng hình ảnh. Với file ảnh, yêu + cầu là .jpg, .gif, hoặc .png và dung lượng tối đa 300KB. +
+ + +
\ No newline at end of file diff --git a/old/template/system/form_store_design/header.html b/old/template/system/form_store_design/header.html new file mode 100644 index 0000000..9424a9b --- /dev/null +++ b/old/template/system/form_store_design/header.html @@ -0,0 +1,30 @@ +
+ + + + + + + + + + +
Thay Logo +
+ +
+ + +
Thay Ảnh header + +
+ +
+ + +
+ + +
\ No newline at end of file diff --git a/old/template/system/form_store_design/other.html b/old/template/system/form_store_design/other.html new file mode 100644 index 0000000..6ad7364 --- /dev/null +++ b/old/template/system/form_store_design/other.html @@ -0,0 +1,93 @@ +
+ + Cảnh báo: Vì các cài đặt sau sẽ được sử dụng ở biến global. Nên hệ thống sẽ cho phép tạo tối đa 20 giá + trị và giới hạn số ký tự không quá 500 mỗi giá trị để đảm bảo website hoạt động tốt. Do vậy chỉ cài đặt các nội + dung thật sự sẽ được hiển thị ở tất cả các nơi (v.d. Header/Footer). + + +
+ + + + + + + + + + + + + + + + + + + + + +
STT Code giao diện Chú thích (cho quản trị viên) Giá trị Hành động
1 global.settings.builpc_category_special_offer Danh sách khuyến mại theo từng danh mục trang Xây dựng cấu hình + + + Cập + nhật + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/old/template/system/form_store_design/popup.html b/old/template/system/form_store_design/popup.html new file mode 100644 index 0000000..3a30a89 --- /dev/null +++ b/old/template/system/form_store_design/popup.html @@ -0,0 +1,57 @@ +
+ + + + + + + + + + + + + + + + + + + + + +
Link nhảy đến + +
File ảnh +
+ +
+ + +
Lựa chọn hiển thị người dùng +
+ + + +
+
Khác + + +
+ + +
\ No newline at end of file diff --git a/old/template/system/pay_method.html b/old/template/system/pay_method.html new file mode 100644 index 0000000..b0e453b --- /dev/null +++ b/old/template/system/pay_method.html @@ -0,0 +1,69 @@ + + +
+
+
+

Các phương thức thanh toán

+ + + Thêm phương thức mới +
+

Cập nhật các phương thức thanh toán mà quý khách muốn hiển thị ở trang thanh toán đơn + hàng. Chỉ các phương + thức ở trạng + thái "Đang hiển thị" được hiển thị ở trang thanh toán. +

+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + +
STTPhân loạiPhương thứcCập nhậtThứ tựLựa chọn
1codThanh toán khi nhận hàng (COD) (#1) +

25-09-2022, 2:42 pm

+

Hurasoft Test

+
10 +
+ + +
+
+
+
+
\ No newline at end of file diff --git a/old/template/system/pay_method_add.html b/old/template/system/pay_method_add.html new file mode 100644 index 0000000..9500922 --- /dev/null +++ b/old/template/system/pay_method_add.html @@ -0,0 +1,96 @@ + + +
+
+

Các phương thức thanh toán

+

Tiền mặt khi nhận hàng (COD)

+
+ + + +
+
+
+ Phân loại (*) +
+ + + + + + + +
+
+
+ + +
+
+ + +
+
+
+ + (cao xếp trước) +
+ +
+
+ Phân loại (*) +
+ + +
+
+
+
+ +
+
+
\ No newline at end of file diff --git a/old/template/system/print.html b/old/template/system/print.html new file mode 100644 index 0000000..3ebc88a --- /dev/null +++ b/old/template/system/print.html @@ -0,0 +1,317 @@ + + +
+
+

Cài đặt In ấn (Print) và Xuất Excel

+

Hướng dẫn: Nên dùng Editor ở phần sửa + template để chỉnh nội dung html rồi + copy/cập + nhật ở đây +

+
+ + + +
+
+ Dành cho khách hàng: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTPhân loạiNội dungHành động
1 +

In giỏ hàng

+

print-user-cart

+
+ Xem + nội + dung + + + Cập + nhật +
2 +

In đơn hàng

+

print-user-order

+
+ Xem + nội + dung + + + Cập + nhật +
3 +

Xuất excel giỏ hàng

+

export-shopping-cart

+
+ Xem + nội + dung + + + Cập + nhật +
4 +

Xuất excel sản phẩm

+

export-product-quote

+
+ Xem + nội + dung + + + Cập + nhật +
5 +

Xuất xây dựng cấu hình máy tính

+

export-buildpc

+
+ Xem + nội + dung + + + Cập + nhật +
6 +

Xuất xây dựng tản nhiệt

+

export-tannhiet

+
+ Xem + nội + dung + + + Cập + nhật +
+
+
+ Dành cho quản trị viên website: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTPhân loạiNội dungHành động
1 + In đơn hàng trong admin
print-admin-order-detail-new +
+ Xem + nội + dung + + + Cập + nhật +
2 + In danh sách đơn hàng trong admin
print-admin-order-list +
+ Xem + nội + dung + + + Cập + nhật +
3 + In admin log
print-admin-admin-log +
+ Xem + nội + dung + + + Cập + nhật +
+ +
+
+
\ No newline at end of file diff --git a/old/template/system/province_list.html b/old/template/system/province_list.html new file mode 100644 index 0000000..b0cbe8d --- /dev/null +++ b/old/template/system/province_list.html @@ -0,0 +1,78 @@ + + +
+
+

Thông tin các tỉnh

+
+ + + +
+
+ Danh sách các tỉnh + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTSystemIDTênĐịa chỉHuyện
16 + An Giang + + Huyện An Phú , Huyện Châu Phú , Huyện Châu Thành , Huyện Chợ Mới , Huyện Phú Tân , Huyện + Tịnh Biên , Huyện Thoại Sơn , + Huyện Tri Tôn , Thành phố Châu Đốc , Thành phố Long Xuyên , Thị xã Tân Châu + +
+

1

+ +
+
16 + An Giang + + Huyện An Phú , Huyện Châu Phú , Huyện Châu Thành , Huyện Chợ Mới , Huyện Phú Tân , Huyện + Tịnh Biên , Huyện Thoại Sơn , + Huyện Tri Tôn , Thành phố Châu Đốc , Thành phố Long Xuyên , Thị xã Tân Châu + +
+

1

+ +
+
+
+
+
\ No newline at end of file diff --git a/old/template/system/settings.html b/old/template/system/settings.html new file mode 100644 index 0000000..056473a --- /dev/null +++ b/old/template/system/settings.html @@ -0,0 +1,168 @@ +
+
+

Tổng hợp các cài đặt cho website

+
+ Cài đặt hiển thị ngoài giao diện: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTNội dungQuản lý
1Header (logo, banner top) + Quản + lý +
2Banner popup + Quản + lý +
3Ảnh/màu nền website + Quản + lý +
4Thông tin cài đặt tùy biến khác + Quản + lý +
+
+
+ Cài đặt dùng cho hệ thống: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTNội dungQuản lý
1Cài đặt hiển thị + Quản + lý +
2Chi nhánh cửa hàng + Quản + lý +
3Chặn IP xấu truy cập website + Quản + lý +
4Email bán hàng/CSKH + Quản + lý +
5Print/ Xuất Excel + Quản + lý +
6Meta SEO trang chủ + Quản + lý +
7Thông tin giới thiệu cửa hàng + Quản + lý +
8Thông tin các tỉnh/thành/quận-huyện + Quản + lý +
9Các phương thức thanh toán + Quản + lý +
10Các phương thức vận chuyển + Quản + lý +
11Cài đặt phí vận chuyển + Quản + lý +
+
+
+
\ No newline at end of file diff --git a/old/template/system/setup.html b/old/template/system/setup.html new file mode 100644 index 0000000..35bbb27 --- /dev/null +++ b/old/template/system/setup.html @@ -0,0 +1,58 @@ + + +
+
+

Cài đặt hiển thị

+
+ + + + + {% if global.url contains 'option=general' %} + + {% include 'system/form_setup/general' %} + + {% elsif global.url contains 'option=homepage' %} + + {% include 'system/form_setup/homepage' %} + + {% elsif global.url contains 'option=pro_category' %} + + {% include 'system/form_setup/pro_category' %} + + {% elsif global.url contains 'option=product' %} + + {% include 'system/form_setup/product' %} + + {% endif %} + +
\ No newline at end of file diff --git a/old/template/system/ship_method.html b/old/template/system/ship_method.html new file mode 100644 index 0000000..df974fa --- /dev/null +++ b/old/template/system/ship_method.html @@ -0,0 +1,74 @@ +
+ + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTPhân loạiPhương thứcCập nhậtChỉnh sửa
1Nhận hàng tại cửa hàngGiao hàng nhanh trong 2 giờ (Nội thành Hà Nội +HCM) +

Admin

+

09-09-2022, 11:53 am

+
+
+ + +
+
1Nhận hàng tại cửa hàngGiao hàng nhanh trong 2 giờ (Nội thành Hà Nội +HCM) +

Admin

+

09-09-2022, 11:53 am

+
+
+ + +
+
+ +
+
\ No newline at end of file diff --git a/old/template/system/ship_method_add.html b/old/template/system/ship_method_add.html new file mode 100644 index 0000000..725ebf8 --- /dev/null +++ b/old/template/system/ship_method_add.html @@ -0,0 +1,104 @@ +
+ + +
+ + + +
+ +
+
+ Phân loại (*) + +
+ + + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+
+
+ +
+
+
+ +
+ +
\ No newline at end of file diff --git a/old/template/system/shipping.html b/old/template/system/shipping.html new file mode 100644 index 0000000..03d1877 --- /dev/null +++ b/old/template/system/shipping.html @@ -0,0 +1,95 @@ + + +
+
+

Cài đặt hệ thống

+
+ + + +
+
+
+
+ + (* Đơn hàng có giá trị nhỏ hơn giá trị này sẽ được áp + dụng biểu + phí theo vùng) +
+ +
+
+
+ Cài đặt phí thu hộ + + + + + + + + + + + + + + + + + +
STTGiá trị đơn hàng dưới mứcPhí thu hộ (VND)Lựa chọn
1 + + + + + + + +
+ +
+
+ + +
+ Hướng dẫn cài phí thu hộ +

Phí thu hộ được tính theo giá trị cần thu hộ, ví dụ:

+

- Từ 0 đến <=1 triệu=30.000đ=> Cần nhập là 1.000.000

+

- Từ >1 triệu đến <= 2 triệu=50.000đ=> Cần nhập là 2.000.000

+

- Từ >2 triệu đến <= 3 triệu=70.000đ=> Cần nhập là 3.000.000

+

- Từ >3 triệu đến <= 4 triệu=90.000đ=> Cần nhập là 4.000.000

+

- Từ >4 triệu đến <= 5 triệu=110.000đ=> Cần nhập là 5.000.000

+

- Từ >5 triệu đến <= 6 triệu=130.000đ=> Cần nhập là 6.000.000

+

- Từ >6 triệu đến <= 7 triệu=150.000đ=> Cần nhập là 7.000.000

+

- Từ >7 triệu đến <= 8 triệu=170.000đ=> Cần nhập là 8.000.000

+

- Từ >8 triệu đến <= 9 triệu=190.000đ=> Cần nhập là 9.000.000

+

- Trên 9 triệu = 0đ => Cần nhập là 10.000.000.000 (nhập số lớn nhất có thể)

+

Để xóa phí nào nó, click "Xóa bỏ" và nhấn "Cập nhật" để lưu lại

+
+
+
+
\ No newline at end of file diff --git a/old/template/system/store_address.html b/old/template/system/store_address.html new file mode 100644 index 0000000..2b420c9 --- /dev/null +++ b/old/template/system/store_address.html @@ -0,0 +1,141 @@ + + +
+
+

Các địa chỉ cửa hàng

+
+ + + +
+
+ Thêm địa chỉ mới +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ Đây là 1 cửa hàng ? + +
+ + + +
+ +
+ Danh sách địa chỉ cửa hàng + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTTên gọiĐịa chỉTỉnhLà 1 cửa hàngERP IDLựa chọn
1HACOM Q7, TP. Hồ Chí Minh500 Nguyễn Thị Thập - Quận 7 - TP. Hồ Chí MinhTP HCMyesTon120 +
+ + + +
+
1HACOM Q7, TP. Hồ Chí Minh500 Nguyễn Thị Thập - Quận 7 - TP. Hồ Chí MinhTP HCMyesTon120 +
+ + + +
+
+
+
+
\ No newline at end of file diff --git a/old/template/system/store_design.html b/old/template/system/store_design.html new file mode 100644 index 0000000..b08c1b6 --- /dev/null +++ b/old/template/system/store_design.html @@ -0,0 +1,63 @@ + + +
+
+

Tùy chỉnh thành phần giao diện

+ +

+ Chú ý: Chức năng này chỉ áp dụng với các giao diện đã được cài đặt cho phép thay đổi 1 số thành phần + của giao diện. Biến template sử dụng $settings (global) +

+
+ + + + {% if global.url contains 'section=header' %} + + {% include 'system/form_store_design/header' %} + + {% elsif global.url contains 'section=popup' %} + + {% include 'system/form_store_design/popup' %} + + {% elsif global.url contains 'section=background' %} + + {% include 'system/form_store_design/background' %} + + {% elsif global.url contains 'section=other' %} + + {% include 'system/form_store_design/other' %} + + {% endif %} +
\ No newline at end of file diff --git a/old/template/tag/add.html b/old/template/tag/add.html new file mode 100644 index 0000000..14816e9 --- /dev/null +++ b/old/template/tag/add.html @@ -0,0 +1,82 @@ +
+ + + +
+ + +
+
+ +
+
+
+ Thông tin tag +
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/old/template/tag/home.html b/old/template/tag/home.html new file mode 100644 index 0000000..f95fe63 --- /dev/null +++ b/old/template/tag/home.html @@ -0,0 +1,79 @@ +
+
+

Danh sách tag

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTTagLink đíchThời gian tạoLựa chọn
1ATO tesst +
+ + xem +
+
20-10-2023, 5:58 pm / Khách +
+ + +
+
1màn hình thời gián đáp ứng 5ms +
+ + xem +
+
20-10-2023, 5:58 pm / Khách +
+ + +
+
+ +
+
\ No newline at end of file diff --git a/old/template/template/add_template.html b/old/template/template/add_template.html new file mode 100644 index 0000000..198684b --- /dev/null +++ b/old/template/template/add_template.html @@ -0,0 +1,73 @@ + + +
+
+
+

Cập nhật template file, giao diện

+ +
+

mobile_detail

+
+ + + +
+
+ Cập nhật lại +
+
+ + v.d. product_detail, account_home +
+ +
+
+
+
+ + v.d. product / other / customer / ajax +
+ +
+
+ hoặc nhập + +
+
+
+
+ +
+ +
+
+
+ +
+
+
\ No newline at end of file diff --git a/old/template/template/edit_template.html b/old/template/template/edit_template.html new file mode 100644 index 0000000..f285665 --- /dev/null +++ b/old/template/template/edit_template.html @@ -0,0 +1,227 @@ +
+ + + +
+
+

Cập nhật template file, giao diện

+

web_mau

+
+ + +
+
+
+
+ +
+

Giao diện mobile

+ +
+
+
+
+
+ other + +
+
+ other + +
+
+ other + +
+
+ other + +
+
+ other + +
+
+ other + +
+
+
+
+
+ +
+

Giao diện pc

+ +
+
+
+
+
+ other + +
+
+ other + +
+
+ other + +
+
+
+
+
+
+
+ +
+

Giao diện Website

+ +
+
+ +
+
+
+
+ +
+

File .js & .css

+ +
+
+ +
+
+
+
+ +
+

Ảnh giao diện

+ +
+
+ +
+
+
+
+
+
+

about_us

+ Xem các bản + cũ +
+ + + +
+
+ +
+ + +
+ +
+
+ +
+ +
+
\ No newline at end of file diff --git a/old/template/template/home.html b/old/template/template/home.html new file mode 100644 index 0000000..8ac0325 --- /dev/null +++ b/old/template/template/home.html @@ -0,0 +1,80 @@ +
+
+

Danh sách giao diện web

+ + + + + Thêm mẫu giao diện mới + + + + + + + + + + + + + + + + + + + + + + + + + +
STTThư mụcTên / miêu tảThông tinSửa giao diệnXem templateQuản lý
1default + Giao diện mặc định +

Mẫu giao diện ban đầu khi tạo website. Mẫu này được dùng khi không có mẫu nào khác được kích + hoạt sử dụng thay thế +

+ + Tải backup giao diện + +
+

Tạo lúc: 23-03-2021, 2:46 pm

+

Trần Khắc Tiệp

+
+ Sửa + giao diện + + + Danh sách file + + Upload file + + +
+
+
\ No newline at end of file diff --git a/old/template/template/set.html b/old/template/template/set.html new file mode 100644 index 0000000..8fb74e2 --- /dev/null +++ b/old/template/template/set.html @@ -0,0 +1,74 @@ + + +
+
+
+

Danh sách template file, giao diện

+ +
+ +

htpstore

+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + +
STTModuleTemplateThứ tựThông tinLựa chọn
10mobile_detail0Tạo lúc bởi: 0 + +
+
+
+
\ No newline at end of file diff --git a/old/template/template/upload_set.html b/old/template/template/upload_set.html new file mode 100644 index 0000000..21d0a5e --- /dev/null +++ b/old/template/template/upload_set.html @@ -0,0 +1,54 @@ + + +
+
+

Cập nhật giao diện

+

htpstore

+
+ + + +
+
+ Cập nhật lại +
+
+ + v.d. xehoi (các file liên quan tới giao diện cần upload + tại + /template/xehoi/ ) +
+ +
+
+
+ + v.d. Giao diện xe hơi +
+ +
+
+ + +
+
+
+ +
+
+
\ No newline at end of file diff --git a/old/template/theme.html b/old/template/theme.html new file mode 100644 index 0000000..ebc644b --- /dev/null +++ b/old/template/theme.html @@ -0,0 +1,42 @@ + + + + + + + Admin Hura Pc + + + + + + + + + + + {% include module_css/index %} + + + + + +
+
+ {% include "other/header" %} +
+ {{ page_content }} +
+
+ +
+
+
+ + {% include javascript/index %} + + + + \ No newline at end of file diff --git a/old/template/tool/home.html b/old/template/tool/home.html new file mode 100644 index 0000000..7da889c --- /dev/null +++ b/old/template/tool/home.html @@ -0,0 +1,70 @@ +
+
+

Công cụ

+

+ Lưu ý: Các công cụ dưới đây thực hiện thao tác áp dụng cho hàng loạt nội dung. Chỉ nên phân quyền cho + phép một số quản + trị viên được sử dụng để tránh dữ liệu bị xóa +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTCông cụLink
1 + Thao tác cho hàng loạt Sản phẩm +

Thực hiện 1 thao tác cho 1 danh sách các Sản phẩm, gồm: Xóa, Hiển thị, Đổi danh mục

+
+ Sử + dụng +
2 + Chuyển Sản phẩm giữa các danh mục +

Chuyển toàn bộ hoặc 1 nhóm Sản phẩm từ danh mục A sang danh mục B

+
+ Sử + dụng +
3 + Cập nhật sản phẩm chưa đủ thuộc tính +

Xem các sản phẩm chưa có đủ thuộc tính để cập nhật nhanh

+
+ Sử + dụng +
4 + Kiểm tra các mã sản phẩm (SKU) chưa có tại website +

Nhập các mã SKU để kiểm tra xem đã có trên website hay chưa

+
+ Sử + dụng +
+
+
\ No newline at end of file diff --git a/old/template/url/redirect.html b/old/template/url/redirect.html new file mode 100644 index 0000000..be897e5 --- /dev/null +++ b/old/template/url/redirect.html @@ -0,0 +1,72 @@ +
+
+

Danh sách link redirect

+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + +
STTLink truy cậpLink đíchRedirect_codeQuản lý
1 + + + + + 301 + + +
+ +
+
\ No newline at end of file diff --git a/old/template/url/redirect_form.html b/old/template/url/redirect_form.html new file mode 100644 index 0000000..fda6f08 --- /dev/null +++ b/old/template/url/redirect_form.html @@ -0,0 +1,47 @@ + + +
+
+

Cập nhật link redirect

+

/bo-phim-chuot-fuhlen-l411-fuhlen-l102

+
+ + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
\ No newline at end of file diff --git a/old/template/url/url_seo.html b/old/template/url/url_seo.html new file mode 100644 index 0000000..71a22bc --- /dev/null +++ b/old/template/url/url_seo.html @@ -0,0 +1,74 @@ +
+
+ +
+

Upload Excel SEO sản phẩm

+ +

Yêu cầu file excel theo đúng Cấu hình cột Excel bên dưới

+
+ + +
+ +
+ + +
+ + + + + + + + + + + + +
File cập nhật + +
Cấu hình cột Excel + + + + + + + + + + + + + + + + + + + + + + + + + + +
Cột Excel Nội dung
A ID Sản phẩm Web
B Mã kho (SKU)
C Thẻ Meta Title
D Thẻ Meta Keyword
E Thẻ Meta Description
+ +
+ + +
+
+
\ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8e59223 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1383 @@ +{ + "name": "admin_hura_8", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "admin_hura_8", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@tailwindcss/cli": "^4.1.17", + "autoprefixer": "^10.4.22", + "daisyui": "^5.5.8", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.17" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/cli": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.17.tgz", + "integrity": "sha512-jUIxcyUNlCC2aNPnyPEWU/L2/ik3pB4fF3auKGXr8AvN3T3OFESVctFKOBoPZQaZJIeUpPn1uCLp0MRxuek8gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.5.1", + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", + "enhanced-resolve": "^5.18.3", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "4.1.17" + }, + "bin": { + "tailwindcss": "dist/index.mjs" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", + "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-x64": "4.1.17", + "@tailwindcss/oxide-freebsd-x64": "4.1.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-x64-musl": "4.1.17", + "@tailwindcss/oxide-wasm32-wasi": "4.1.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", + "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", + "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", + "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", + "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", + "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.2.tgz", + "integrity": "sha512-PxSsosKQjI38iXkmb3d0Y32efqyA0uW4s41u4IVBsLlWLhCiYNpH/AfNOVWRqCQBlD8TFJTz6OUWNd4DFJCnmw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001759", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", + "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/daisyui": { + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.8.tgz", + "integrity": "sha512-6psL9jIEOFOw68V10j/BKCWcRgx8dh81mmNxShr+g7HDM6UHNoPharlp9zq/PQkHNuGU1ZQsajR3HgpvavbRKQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/saadeghi/daisyui?sponsor=1" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.265", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.265.tgz", + "integrity": "sha512-B7IkLR1/AE+9jR2LtVF/1/6PFhY5TlnEHnlrKmGk7PvkJibg5jr+mLXLLzq3QYl6PA1T/vLDthQPqIPAlS/PPA==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d2e6901 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "admin_hura_8", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "build:js": "vite build", + "build:css": "tailwindcss -i ./assets/script/tailwind-daisyui.css -o ./assets/script/hura-lib.css", + "watch:css": "tailwindcss -i ./assets/script/tailwind-daisyui.css -o ./assets/script/hura-lib.css --watch", + "build": "npm run build:js && npm run build:css", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://repo.hurasoft.com/tieptk/admin_hura_8.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "module", + "devDependencies": { + "@tailwindcss/cli": "^4.1.17", + "autoprefixer": "^10.4.22", + "daisyui": "^5.5.8", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.17" + } +} diff --git a/package/composer.json b/package/composer.json new file mode 100644 index 0000000..b5f204b --- /dev/null +++ b/package/composer.json @@ -0,0 +1,8 @@ +{ + "require": { + "ext-json": "*", + "ext-mysqli": "*", + "ext-zip": "*", + "liquid/liquid": "^1.4" + } +} diff --git a/package/composer.lock b/package/composer.lock new file mode 100644 index 0000000..05abcdb --- /dev/null +++ b/package/composer.lock @@ -0,0 +1,92 @@ +{ + "_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": "06d921b0b448aee0ee0d4d3db3de06d6", + "packages": [ + { + "name": "liquid/liquid", + "version": "1.4.44", + "source": { + "type": "git", + "url": "https://github.com/kalimatas/php-liquid.git", + "reference": "1b74ad64001d21f27223849624f7aab0c379815a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kalimatas/php-liquid/zipball/1b74ad64001d21f27223849624f7aab0c379815a", + "reference": "1b74ad64001d21f27223849624f7aab0c379815a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": ">=2.47", + "friendsofphp/php-cs-fixer": "^3.75", + "infection/infection": ">=0.17.6", + "php-coveralls/php-coveralls": "^2.8", + "phpunit/phpunit": "^9.6.23" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Liquid\\": "src/Liquid" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guz Alexander", + "email": "kalimatas@gmail.com", + "homepage": "http://guzalexander.com" + }, + { + "name": "Harald Hanek" + }, + { + "name": "Mateo Murphy" + }, + { + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com", + "homepage": "https://www.alexeykopytko.com/" + } + ], + "description": "Liquid template engine for PHP", + "homepage": "https://github.com/kalimatas/php-liquid", + "keywords": [ + "liquid", + "template" + ], + "support": { + "issues": "https://github.com/kalimatas/php-liquid/issues", + "source": "https://github.com/kalimatas/php-liquid/tree/1.4.44" + }, + "time": "2025-05-31T10:45:02+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "ext-json": "*", + "ext-mysqli": "*", + "ext-zip": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/package/vendor/autoload.php b/package/vendor/autoload.php new file mode 100644 index 0000000..5985488 --- /dev/null +++ b/package/vendor/autoload.php @@ -0,0 +1,22 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ + private $vendorDir; + + // PSR-4 + /** + * @var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var list + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array>> + */ + private $prefixesPsr0 = array(); + /** + * @var list + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var array + */ + private $missingClasses = array(); + + /** @var string|null */ + private $apcuPrefix; + + /** + * @var array + */ + private static $registeredLoaders = array(); + + /** + * @param string|null $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return array> + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return list + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return list + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return array Array of classname => path + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders keyed by their corresponding vendor directories. + * + * @return array + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/package/vendor/composer/InstalledVersions.php b/package/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000..2052022 --- /dev/null +++ b/package/vendor/composer/InstalledVersions.php @@ -0,0 +1,396 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to + * @internal + */ + private static $selfDir = null; + + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool + */ + private static $installedIsLocalDir; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + + // when using reload, we disable the duplicate protection to ensure that self::$installed data is + // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, + // so we have to assume it does not, and that may result in duplicate data being returned when listing + // all installed packages for example + self::$installedIsLocalDir = false; + } + + /** + * @return string + */ + private static function getSelfDir() + { + if (self::$selfDir === null) { + self::$selfDir = strtr(__DIR__, '\\', '/'); + } + + return self::$selfDir; + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + $copiedLocalDir = false; + + if (self::$canGetVendors) { + $selfDir = self::getSelfDir(); + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + $vendorDir = strtr($vendorDir, '\\', '/'); + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + self::$installedByVendor[$vendorDir] = $required; + $installed[] = $required; + if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { + self::$installed = $required; + self::$installedIsLocalDir = true; + } + } + if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { + $copiedLocalDir = true; + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array() && !$copiedLocalDir) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/package/vendor/composer/LICENSE b/package/vendor/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/package/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/package/vendor/composer/autoload_classmap.php b/package/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..0fb0a2c --- /dev/null +++ b/package/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ + $vendorDir . '/composer/InstalledVersions.php', +); diff --git a/package/vendor/composer/autoload_namespaces.php b/package/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..15a2ff3 --- /dev/null +++ b/package/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/liquid/liquid/src/Liquid'), +); diff --git a/package/vendor/composer/autoload_real.php b/package/vendor/composer/autoload_real.php new file mode 100644 index 0000000..2960ca8 --- /dev/null +++ b/package/vendor/composer/autoload_real.php @@ -0,0 +1,38 @@ +register(true); + + return $loader; + } +} diff --git a/package/vendor/composer/autoload_static.php b/package/vendor/composer/autoload_static.php new file mode 100644 index 0000000..5ed6bd2 --- /dev/null +++ b/package/vendor/composer/autoload_static.php @@ -0,0 +1,36 @@ + + array ( + 'Liquid\\' => 7, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Liquid\\' => + array ( + 0 => __DIR__ . '/..' . '/liquid/liquid/src/Liquid', + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit06d921b0b448aee0ee0d4d3db3de06d6::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit06d921b0b448aee0ee0d4d3db3de06d6::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit06d921b0b448aee0ee0d4d3db3de06d6::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/package/vendor/composer/installed.json b/package/vendor/composer/installed.json new file mode 100644 index 0000000..6f810be --- /dev/null +++ b/package/vendor/composer/installed.json @@ -0,0 +1,78 @@ +{ + "packages": [ + { + "name": "liquid/liquid", + "version": "1.4.44", + "version_normalized": "1.4.44.0", + "source": { + "type": "git", + "url": "https://github.com/kalimatas/php-liquid.git", + "reference": "1b74ad64001d21f27223849624f7aab0c379815a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kalimatas/php-liquid/zipball/1b74ad64001d21f27223849624f7aab0c379815a", + "reference": "1b74ad64001d21f27223849624f7aab0c379815a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": ">=2.47", + "friendsofphp/php-cs-fixer": "^3.75", + "infection/infection": ">=0.17.6", + "php-coveralls/php-coveralls": "^2.8", + "phpunit/phpunit": "^9.6.23" + }, + "time": "2025-05-31T10:45:02+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Liquid\\": "src/Liquid" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guz Alexander", + "email": "kalimatas@gmail.com", + "homepage": "http://guzalexander.com" + }, + { + "name": "Harald Hanek" + }, + { + "name": "Mateo Murphy" + }, + { + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com", + "homepage": "https://www.alexeykopytko.com/" + } + ], + "description": "Liquid template engine for PHP", + "homepage": "https://github.com/kalimatas/php-liquid", + "keywords": [ + "liquid", + "template" + ], + "support": { + "issues": "https://github.com/kalimatas/php-liquid/issues", + "source": "https://github.com/kalimatas/php-liquid/tree/1.4.44" + }, + "install-path": "../liquid/liquid" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/package/vendor/composer/installed.php b/package/vendor/composer/installed.php new file mode 100644 index 0000000..2bd4248 --- /dev/null +++ b/package/vendor/composer/installed.php @@ -0,0 +1,32 @@ + array( + 'name' => '__root__', + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => 'b4b7a452fcfd9396ee513ef34fff4444f4537a15', + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev' => true, + ), + 'versions' => array( + '__root__' => array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => 'b4b7a452fcfd9396ee513ef34fff4444f4537a15', + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'liquid/liquid' => array( + 'pretty_version' => '1.4.44', + 'version' => '1.4.44.0', + 'reference' => '1b74ad64001d21f27223849624f7aab0c379815a', + 'type' => 'library', + 'install_path' => __DIR__ . '/../liquid/liquid', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/package/vendor/composer/platform_check.php b/package/vendor/composer/platform_check.php new file mode 100644 index 0000000..d2225c7 --- /dev/null +++ b/package/vendor/composer/platform_check.php @@ -0,0 +1,25 @@ += 70400)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + throw new \RuntimeException( + 'Composer detected issues in your platform: ' . implode(' ', $issues) + ); +} diff --git a/package/vendor/liquid/liquid/.git-blame-ignore-revs b/package/vendor/liquid/liquid/.git-blame-ignore-revs new file mode 100644 index 0000000..d9ba950 --- /dev/null +++ b/package/vendor/liquid/liquid/.git-blame-ignore-revs @@ -0,0 +1,5 @@ +# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view +# https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt + +72c7e3f4e34d2ed474b1399f2a7ab427fb6f3b14 +d837112369c0193def1c27636da1b9a8df5c98f3 diff --git a/package/vendor/liquid/liquid/.php-cs-fixer.dist.php b/package/vendor/liquid/liquid/.php-cs-fixer.dist.php new file mode 100644 index 0000000..7731278 --- /dev/null +++ b/package/vendor/liquid/liquid/.php-cs-fixer.dist.php @@ -0,0 +1,62 @@ +setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) + ->setRiskyAllowed(true) + ->setRules([ + '@PSR2' => true, + 'psr_autoloading' => true, + 'no_unreachable_default_argument_value' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'phpdoc_add_missing_param_annotation' => true, + 'phpdoc_order' => true, + 'semicolon_after_instruction' => true, + 'whitespace_after_comma_in_array' => true, + 'header_comment' => ['header' => $header], + 'php_unit_construct' => true, + 'php_unit_dedicate_assert' => true, + 'php_unit_dedicate_assert_internal_type' => true, + 'php_unit_expectation' => true, + 'php_unit_mock_short_will_return' => true, + 'php_unit_mock' => true, + 'php_unit_namespaced' => true, + 'php_unit_no_expectation_annotation' => true, + "phpdoc_order_by_value" => ['annotations' => ['covers']], + 'php_unit_set_up_tear_down_visibility' => true, + 'php_unit_test_case_static_method_calls' => ['call_type' => 'this'], + 'no_whitespace_in_blank_line' => true, + 'nullable_type_declaration_for_default_null_value' => true, + 'array_syntax' => ['syntax' => 'short'], + 'trailing_comma_in_multiline' => ['elements' => ['arrays']], + 'binary_operator_spaces' => ['default' => 'at_least_single_space'], + ]) + ->setIndent("\t") + ->setFinder( + PhpCsFixer\Finder::create() + ->in(__DIR__) + ->append([__FILE__]) + ) +; + + +return $config; diff --git a/package/vendor/liquid/liquid/CHANGELOG.md b/package/vendor/liquid/liquid/CHANGELOG.md new file mode 100644 index 0000000..c2143dd --- /dev/null +++ b/package/vendor/liquid/liquid/CHANGELOG.md @@ -0,0 +1,107 @@ +## master + +## 1.4.8 (2018-03-22) + + * Now we return null for missing properties, like we do for missing keys for arrays. + +## 1.4.7 (2018-02-09) + + * Paginate tag shall now respect request parameters. + * It is now possible to set a custom query param for the paginate tag. + * Page number will now never go overboard. + +## 1.4.6 (2018-02-07) + + * TagPaginate shall not pollute the global scope, but work in own scope. + * TagPaginate errors if no collection present instead of vague warning. + +## 1.4.5 (2017-12-12) + + * Capture tag shall save a variable in the global context. + +## 1.4.4 (2017-11-03) + + * TagUnless is an inverted TagIf: simplified implementation + * Allow dashes in filenames + +## 1.4.3 (2017-10-10) + + * `escape` and `escape_once` filters now escape everything, but arrays + * New standard filter for explicit string conversion + +## 1.4.2 (2017-10-09) + + * Better caching for non-extending templates + * Simplified 'assign' tag to use rules for variables + * Now supporting PHP 7.2 + * Different types of exception depending on the case + * Filterbank will not call instance methods statically + * Callback-type filters + +## 1.4.1 (2017-09-28) + + * Unquoted template names in 'include' tag, as in Jekyll + * Caching now works correctly with 'extends' tag + +## 1.4.0 (2017-09-25) + + * Dropped support for EOL'ed versions of PHP (< 5.6) + * Arrays won't be silently cast to string as 'Array' anymore + * Complex objects could now be passed between templates and to filters + * Additional test coverage + +## 1.3.1 (2017-09-23) + + * Support for numeric and variable array indicies + * Support loop break and continue + * Allow looping over extended ranges + * Math filters now work with floats + * Fixed 'default' filter + * Local cache with data stored in a private variable + * Virtual file system to get inversion of control and DI + * Lots of tests with the coverage upped to 97% + * Small bug fixes and various enhancements + +## 1.3.0 (2017-07-17) + + * Support Traversable loops and filters + * Fix date filter for format with colon + * Various minor improvements and bugs fixes + +## 1.2.1 (2016-12-12) + + * Remove content injection from $_GET. + * Add PHP 5.6, 7.0, 7.1 to Travis file. + +## 1.2 (2016-06-11) + + * Added "ESCAPE_BY_DEFAULT" setting for context-aware auto-escaping. + * Made "Context" work with plain objects. + * "escape" now uses "htmlentities". + * Fixed "escape_now". + +## 1.1 (2015-06-01) + + * New tags: "paginate", "unless", "ifchanged" were added + * Added support for "for in (range)" syntax + * Added support for multiple conditions in if statements + * Added support for hashes/objects in for loops + +## 1.0 (2014-09-07) + + * Add namespaces + * Add composer support + * Implement new standard filters + * Add 'raw' tag + +## 0.9.2 (2012-08-15) + + * context->set allows now global vars + * Allow Templatenames with Fileextension + * Tag 'extends' supports now multiple inheritance + * Clean up code, change all variables and methods to camelCase + +## 0.9.1 (2012-05-12) + + * added the extends and block filter + * Initial release diff --git a/package/vendor/liquid/liquid/LICENSE b/package/vendor/liquid/liquid/LICENSE new file mode 100644 index 0000000..0e3bcec --- /dev/null +++ b/package/vendor/liquid/liquid/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2014 Guz Alexander, http://guzalexander.com +Copyright (c) 2011, 2012 Harald Hanek, http://www.delacap.com +Copyright (c) 2006 Mateo Murphy + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/package/vendor/liquid/liquid/README.md b/package/vendor/liquid/liquid/README.md new file mode 100644 index 0000000..2a66e46 --- /dev/null +++ b/package/vendor/liquid/liquid/README.md @@ -0,0 +1,139 @@ +# Liquid template engine for PHP [![CI](https://github.com/kalimatas/php-liquid/actions/workflows/tests.yaml/badge.svg)](https://github.com/kalimatas/php-liquid/actions/workflows/tests.yaml) [![Coverage Status](https://coveralls.io/repos/github/kalimatas/php-liquid/badge.svg?branch=master)](https://coveralls.io/github/kalimatas/php-liquid?branch=master) [![Total Downloads](https://poser.pugx.org/liquid/liquid/downloads.svg)](https://packagist.org/packages/liquid/liquid) + +Liquid is a PHP port of the [Liquid template engine for Ruby](https://github.com/Shopify/liquid), which was written by Tobias Lutke. Although there are many other templating engines for PHP, including Smarty (from which Liquid was partially inspired), Liquid had some advantages that made porting worthwhile: + + * Readable and human friendly syntax, that is usable in any type of document, not just html, without need for escaping. + * Quick and easy to use and maintain. + * 100% secure, no possibility of embedding PHP code. + * Clean OO design, rather than the mix of OO and procedural found in other templating engines. + * Seperate compiling and rendering stages for improved performance. + * Easy to extend with your own "tags and filters":https://github.com/harrydeluxe/php-liquid/wiki/Liquid-for-programmers. + * 100% Markup compatibility with a Ruby templating engine, making templates usable for either. + * Unit tested: Liquid is fully unit-tested. The library is stable and ready to be used in large projects. + +## Why Liquid? + +Why another templating library? + +Liquid was written to meet three templating library requirements: good performance, easy to extend, and simply to use. + +## Installing + +You can install this lib via [composer](https://getcomposer.org/): + + composer require liquid/liquid + +## Example template + + {% if products %} +
    + {% for product in products %} +
  • +

    {{ product.name }}

    + Only {{ product.price | price }} + + {{ product.description | prettyprint | paragraph }} + + {{ 'it rocks!' | paragraph }} + +
  • + {% endfor %} +
+ {% endif %} + +## How to use Liquid + +The main class is `Liquid::Template` class. There are two separate stages of working with Liquid templates: parsing and rendering. Here is a simple example: + + use Liquid\Template; + + $template = new Template(); + $template->parse("Hello, {{ name }}!"); + echo $template->render(array('name' => 'Alex')); + + // Will echo + // Hello, Alex! + +To find more examples have a look at the `examples` directory or at the original Ruby implementation repository's [wiki page](https://github.com/Shopify/liquid/wiki). + +## Advanced usage + +You would probably want to add a caching layer (at very least a request-wide one), enable context-aware automatic escaping, and do load includes from disk with full file names. + + use Liquid\Liquid; + use Liquid\Template; + use Liquid\Cache\Local; + + Liquid::set('INCLUDE_SUFFIX', ''); + Liquid::set('INCLUDE_PREFIX', ''); + Liquid::set('INCLUDE_ALLOW_EXT', true); + Liquid::set('ESCAPE_BY_DEFAULT', true); + + $template = new Template(__DIR__.'/protected/templates/'); + + $template->parse("Hello, {% include 'honorific.html' %}{{ plain-html | raw }} {{ comment-with-xss }}"); + $template->setCache(new Local()); + + echo $template->render([ + 'name' => 'Alex', + 'plain-html' => 'Your comment was:', + 'comment-with-xss' => '', + ]); + +Will output: + + Hello, Mx. Alex + Your comment was: <script>alert();</script> + +Note that automatic escaping is not a standard Liquid feature: use with care. + +Similarly, the following snippet will parse and render `templates/home.liquid` while storing parsing results in a class-local cache: + + \Liquid\Liquid::set('INCLUDE_PREFIX', ''); + + $template = new \Liquid\Template(__DIR__ . '/protected/templates'); + $template->setCache(new \Liquid\Cache\Local()); + echo $template->parseFile('home')->render(); + +If you render the same template over and over for at least a dozen of times, the class-local cache will give you a slight speed up in range of some milliseconds per render depending on a complexity of your template. + +You should probably extend `Liquid\Template` to initialize everything you do with `Liquid::set` in one place. + +### Custom filters + +Adding filters has never been easier. + + $template = new Template(); + $template->registerFilter('absolute_url', function ($arg) { + return "https://www.example.com$arg"; + }); + $template->parse("{{ my_url | absolute_url }}"); + echo $template->render(array( + 'my_url' => '/test' + )); + // expect: https://www.example.com/test + +## Requirements + + * PHP 7.4+ + +Some earlier versions could be used with PHP 5.3/5.4/5.5/5.6, though they're not supported anymore. + +## Issues + +Have a bug? Please create an issue here on GitHub! + +[https://github.com/kalimatas/php-liquid/issues](https://github.com/kalimatas/php-liquid/issues) + +## Fork notes + +This fork is based on [php-liquid](https://github.com/harrydeluxe/php-liquid) by Harald Hanek. + +It contains several improvements: + + * namespaces + * installing via composer + * new standard filters + * `raw` tag added + +Any help is appreciated! diff --git a/package/vendor/liquid/liquid/composer.json b/package/vendor/liquid/liquid/composer.json new file mode 100644 index 0000000..3fdc946 --- /dev/null +++ b/package/vendor/liquid/liquid/composer.json @@ -0,0 +1,58 @@ +{ + "name": "liquid/liquid", + "type": "library", + "description": "Liquid template engine for PHP", + "keywords": [ + "liquid", + "template" + ], + "homepage": "https://github.com/kalimatas/php-liquid", + "license": "MIT", + "authors": [ + { + "name": "Guz Alexander", + "email": "kalimatas@gmail.com", + "homepage": "http://guzalexander.com" + }, + { + "name": "Harald Hanek" + }, + { + "name": "Mateo Murphy" + }, + { + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com", + "homepage": "https://www.alexeykopytko.com/" + } + ], + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": ">=2.47", + "friendsofphp/php-cs-fixer": "^3.75", + "infection/infection": ">=0.17.6", + "php-coveralls/php-coveralls": "^2.8", + "phpunit/phpunit": "^9.6.23" + }, + "config": { + "sort-packages": true, + "allow-plugins": true + }, + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Liquid\\": "src/Liquid" + } + }, + "autoload-dev": { + "psr-4": { + "Liquid\\": "tests/Liquid" + } + } +} diff --git a/package/vendor/liquid/liquid/infection.json.dist b/package/vendor/liquid/liquid/infection.json.dist new file mode 100644 index 0000000..51839ef --- /dev/null +++ b/package/vendor/liquid/liquid/infection.json.dist @@ -0,0 +1,8 @@ +{ + "timeout": 2, + "source": { + "directories": [ + "src" + ] + } +} diff --git a/package/vendor/liquid/liquid/phpunit.xml.dist b/package/vendor/liquid/liquid/phpunit.xml.dist new file mode 100644 index 0000000..29b38ac --- /dev/null +++ b/package/vendor/liquid/liquid/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + + tests/ + + + + + + src/ + + + + + + + diff --git a/package/vendor/liquid/liquid/src/Liquid/AbstractBlock.php b/package/vendor/liquid/liquid/src/Liquid/AbstractBlock.php new file mode 100644 index 0000000..45afee0 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/AbstractBlock.php @@ -0,0 +1,285 @@ +nodelist; + } + + /** + * Parses the given tokens + * + * @param array $tokens + * + * @throws \Liquid\LiquidException + * @return void + */ + public function parse(array &$tokens) + { + // Constructor is not reliably called by subclasses, so we need to ensure these are set + $this->startRegexp ??= new Regexp('/^' . Liquid::get('TAG_START') . '/'); + $this->tagRegexp ??= new Regexp('/^' . Liquid::get('TAG_START') . Liquid::get('WHITESPACE_CONTROL') . '?\s*(\w+)\s*(.*?)' . Liquid::get('WHITESPACE_CONTROL') . '?' . Liquid::get('TAG_END') . '$/s'); + $this->variableStartRegexp ??= new Regexp('/^' . Liquid::get('VARIABLE_START') . '/'); + + $startRegexp = $this->startRegexp; + $tagRegexp = $this->tagRegexp; + $variableStartRegexp = $this->variableStartRegexp; + + $this->nodelist = []; + + $tags = Template::getTags(); + + for ($i = 0, $n = count($tokens); $i < $n; $i++) { + if ($tokens[$i] === null) { + continue; + } + $token = $tokens[$i]; + $tokens[$i] = null; + + if ($startRegexp->match($token)) { + $this->whitespaceHandler($token); + if ($tagRegexp->match($token)) { + // If we found the proper block delimitor just end parsing here and let the outer block proceed + if ($tagRegexp->matches[1] == $this->blockDelimiter()) { + $this->endTag(); + return; + } + + $tagName = null; + if (array_key_exists($tagRegexp->matches[1], $tags)) { + $tagName = $tags[$tagRegexp->matches[1]]; + } else { + $tagName = self::TAG_PREFIX . ucwords($tagRegexp->matches[1]); + $tagName = (class_exists($tagName) === true) ? $tagName : null; + } + + if ($tagName !== null) { + $this->nodelist[] = new $tagName($tagRegexp->matches[2], $tokens, $this->fileSystem); + if ($tagRegexp->matches[1] == 'extends') { + return; + } + } else { + $this->unknownTag($tagRegexp->matches[1], $tagRegexp->matches[2], $tokens); + } + } else { + throw new ParseException("Tag $token was not properly terminated (won't match $tagRegexp)"); + } + } elseif ($variableStartRegexp->match($token)) { + $this->whitespaceHandler($token); + $this->nodelist[] = $this->createVariable($token); + } else { + // This is neither a tag or a variable, proceed with an ltrim + if (self::$trimWhitespace) { + $token = ltrim($token); + } + + self::$trimWhitespace = false; + $this->nodelist[] = $token; + } + } + + $this->assertMissingDelimitation(); + } + + /** + * Handle the whitespace. + * + * @param string $token + */ + protected function whitespaceHandler($token) + { + $this->whitespaceControl ??= Liquid::get('WHITESPACE_CONTROL'); + + /* + * This assumes that TAG_START is always '{%', and a whitespace control indicator + * is exactly one character long, on a third position. + */ + if ($token[2] === $this->whitespaceControl) { + $previousToken = end($this->nodelist); + if (is_string($previousToken)) { // this can also be a tag or a variable + $this->nodelist[key($this->nodelist)] = rtrim($previousToken); + } + } + + /* + * This assumes that TAG_END is always '%}', and a whitespace control indicator + * is exactly one character long, on a third position from the end. + */ + self::$trimWhitespace = $token[-3] === $this->whitespaceControl; + } + + /** + * Render the block. + * + * @param Context $context + * + * @return string + */ + public function render(Context $context) + { + return $this->renderAll($this->nodelist, $context); + } + + /** + * Renders all the given nodelist's nodes + * + * @param array $list + * @param Context $context + * + * @return string + */ + protected function renderAll(array $list, Context $context) + { + $result = ''; + + foreach ($list as $token) { + if (is_object($token) && method_exists($token, 'render')) { + $value = $token->render($context); + } else { + $value = $token; + } + + if (is_array($value)) { + $value = htmlspecialchars(implode($value)); + } + + $result .= $value; + + if (isset($context->registers['break'])) { + break; + } + if (isset($context->registers['continue'])) { + break; + } + + $context->tick(); + } + + return $result; + } + + /** + * An action to execute when the end tag is reached + */ + protected function endTag() + { + // Do nothing by default + } + + /** + * Handler for unknown tags + * + * @param string $tag + * @param string $params + * @param array $tokens + * + * @throws \Liquid\Exception\ParseException + */ + protected function unknownTag($tag, $params, array $tokens) + { + switch ($tag) { + case 'else': + throw new ParseException($this->blockName() . " does not expect else tag"); + case 'end': + throw new ParseException("'end' is not a valid delimiter for " . $this->blockName() . " tags. Use " . $this->blockDelimiter()); + default: + throw new ParseException("Unknown tag $tag"); + } + } + + /** + * This method is called at the end of parsing, and will throw an error unless + * this method is subclassed, like it is for Document + * + * @throws \Liquid\Exception\ParseException + * @return bool + */ + protected function assertMissingDelimitation() + { + throw new ParseException($this->blockName() . " tag was never closed"); + } + + /** + * Returns the string that delimits the end of the block + * + * @return string + */ + protected function blockDelimiter() + { + return "end" . $this->blockName(); + } + + /** + * Returns the name of the block + * + * @return string + */ + private function blockName() + { + $reflection = new \ReflectionClass($this); + return str_replace('tag', '', strtolower($reflection->getShortName())); + } + + /** + * Create a variable for the given token + * + * @param string $token + * + * @throws \Liquid\Exception\ParseException + * @return Variable + */ + private function createVariable($token) + { + $this->variableRegexp ??= new Regexp('/^' . Liquid::get('VARIABLE_START') . Liquid::get('WHITESPACE_CONTROL') . '?(.*?)' . Liquid::get('WHITESPACE_CONTROL') . '?' . Liquid::get('VARIABLE_END') . '$/s'); + + if ($this->variableRegexp->match($token)) { + return new Variable($this->variableRegexp->matches[1]); + } + + throw new ParseException("Variable $token was not properly terminated"); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/AbstractTag.php b/package/vendor/liquid/liquid/src/Liquid/AbstractTag.php new file mode 100644 index 0000000..0b47418 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/AbstractTag.php @@ -0,0 +1,109 @@ +markup = $markup; + $this->fileSystem = $fileSystem; + $this->config = &Liquid::$config; + + $this->parse($tokens); + } + + /** + * Parse the given tokens. + * + * @param array $tokens + */ + public function parse(array &$tokens) + { + // Do nothing by default + } + + /** + * Render the tag with the given context. + * + * @param Context $context + * + * @return string + */ + abstract public function render(Context $context); + + /** + * Extracts tag attributes from a markup string. + * + * @param string $markup + */ + protected function extractAttributes($markup) + { + $this->attributes = []; + + $attributeRegexp = new Regexp(Liquid::get('TAG_ATTRIBUTES')); + + $matches = $attributeRegexp->scan($markup); + + foreach ($matches as $match) { + $this->attributes[$match[0]] = $match[1]; + } + } + + /** + * Returns the name of the tag. + * + * @return string + */ + protected function name() + { + return strtolower(get_class($this)); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Cache.php b/package/vendor/liquid/liquid/src/Liquid/Cache.php new file mode 100644 index 0000000..3bfb6ac --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Cache.php @@ -0,0 +1,78 @@ +expire = $options['cache_expire']; + } + + if (isset($options['cache_prefix'])) { + $this->prefix = $options['cache_prefix']; + } + } + + /** + * Retrieves a value from cache with a specified key. + * + * @param string $key a unique key identifying the cached value + * @param bool $unserialize + * + * @return mixed|boolean the value stored in cache, false if the value is not in the cache or expired. + */ + abstract public function read($key, $unserialize = true); + + /** + * Check if specified key exists in cache. + * + * @param string $key a unique key identifying the cached value + * + * @return boolean true if the key is in cache, false otherwise + */ + abstract public function exists($key); + + /** + * Stores a value identified by a key in cache. + * + * @param string $key the key identifying the value to be cached + * @param mixed $value the value to be cached + * @param bool $serialize + * + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + abstract public function write($key, $value, $serialize = true); + + /** + * Deletes all values from cache. + * + * @param bool $expiredOnly + * + * @return boolean whether the flush operation was successful. + */ + abstract public function flush($expiredOnly = false); +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Cache/Apc.php b/package/vendor/liquid/liquid/src/Liquid/Cache/Apc.php new file mode 100644 index 0000000..0256ad6 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Cache/Apc.php @@ -0,0 +1,74 @@ +prefix . $key); + } + + /** + * {@inheritdoc} + */ + public function exists($key) + { + apc_fetch($this->prefix . $key, $success); + return (bool) $success; + } + + /** + * {@inheritdoc} + */ + public function write($key, $value, $serialize = true) + { + return apc_store($this->prefix . $key, $value, $this->expire); + } + + /** + * {@inheritdoc} + */ + public function flush($expiredOnly = false) + { + return apc_clear_cache('user'); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Cache/File.php b/package/vendor/liquid/liquid/src/Liquid/Cache/File.php new file mode 100644 index 0000000..1ece9e3 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Cache/File.php @@ -0,0 +1,106 @@ +path = realpath($options['cache_dir']) . DIRECTORY_SEPARATOR; + } else { + throw new NotFoundException('Cachedir not exists or not writable'); + } + } + + /** + * {@inheritdoc} + */ + public function read($key, $unserialize = true) + { + if (!$this->exists($key)) { + return false; + } + + if ($unserialize) { + return unserialize(file_get_contents($this->path . $this->prefix . $key)); + } + + return file_get_contents($this->path . $this->prefix . $key); + } + + /** + * {@inheritdoc} + */ + public function exists($key) + { + $cacheFile = $this->path . $this->prefix . $key; + + if (!file_exists($cacheFile) || filemtime($cacheFile) + $this->expire < time()) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function write($key, $value, $serialize = true) + { + $bytes = file_put_contents($this->path . $this->prefix . $key, $serialize ? serialize($value) : $value); + $this->gc(); + + return $bytes !== false; + } + + /** + * {@inheritdoc} + */ + public function flush($expiredOnly = false) + { + foreach (glob($this->path . $this->prefix . '*') as $file) { + if ($expiredOnly) { + if (filemtime($file) + $this->expire < time()) { + unlink($file); + } + } else { + unlink($file); + } + } + } + + /** + * {@inheritdoc} + */ + protected function gc() + { + $this->flush(true); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Cache/Local.php b/package/vendor/liquid/liquid/src/Liquid/Cache/Local.php new file mode 100644 index 0000000..933da51 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Cache/Local.php @@ -0,0 +1,60 @@ +cache[$key])) { + return $this->cache[$key]; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function exists($key) + { + return isset($this->cache[$key]); + } + + /** + * {@inheritdoc} + */ + public function write($key, $value, $serialize = true) + { + $this->cache[$key] = $value; + return true; + } + + /** + * {@inheritdoc} + */ + public function flush($expiredOnly = false) + { + $this->cache = []; + return true; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Context.php b/package/vendor/liquid/liquid/src/Liquid/Context.php new file mode 100644 index 0000000..066427b --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Context.php @@ -0,0 +1,458 @@ +assigns = [$assigns]; + $this->registers = $registers; + $this->filterbank = new Filterbank($this); + + // first empty array serves as source for overrides, e.g. as in TagDecrement + $this->environments = [[], []]; + + if (Liquid::get('EXPOSE_SERVER')) { + $this->environments[1] = $_SERVER; + } else { + $this->environments[1] = array_filter( + $_SERVER, + function ($key) { + return in_array( + $key, + (array)Liquid::get('SERVER_SUPERGLOBAL_WHITELIST') + ); + }, + ARRAY_FILTER_USE_KEY + ); + } + } + + /** + * Sets a tick function, this function is called sometimes while liquid is rendering a template. + * + * @param callable $tickFunction + */ + public function setTickFunction(callable $tickFunction) + { + $this->tickFunction = $tickFunction; + } + + /** + * Add a filter to the context + * + * @param mixed $filter + */ + public function addFilters($filter, ?callable $callback = null) + { + $this->filterbank->addFilter($filter, $callback); + } + + /** + * Invoke the filter that matches given name + * + * @param string $name The name of the filter + * @param mixed $value The value to filter + * @param array $args Additional arguments for the filter + * + * @return string + */ + public function invoke($name, $value, array $args = []) + { + try { + return $this->filterbank->invoke($name, $value, $args); + } catch (\TypeError $typeError) { + throw new LiquidException($typeError->getMessage(), 0, $typeError); + } + } + + /** + * Merges the given assigns into the current assigns + * + * @param array $newAssigns + */ + public function merge($newAssigns) + { + $this->assigns[0] = array_merge($this->assigns[0], $newAssigns); + } + + /** + * Push new local scope on the stack. + * + * @return bool + */ + public function push() + { + array_unshift($this->assigns, []); + return true; + } + + /** + * Pops the current scope from the stack. + * + * @throws LiquidException + * @return bool + */ + public function pop() + { + if (count($this->assigns) == 1) { + throw new LiquidException('No elements to pop'); + } + + array_shift($this->assigns); + } + + /** + * Replaces [] + * + * @param string + * @param mixed $key + * + * @return mixed + */ + public function get($key) + { + return $this->resolve($key); + } + + /** + * Replaces []= + * + * @param string $key + * @param mixed $value + * @param bool $global + */ + public function set($key, $value, $global = false) + { + if ($global) { + for ($i = 0; $i < count($this->assigns); $i++) { + $this->assigns[$i][$key] = $value; + } + } else { + $this->assigns[0][$key] = $value; + } + } + + /** + * Returns true if the given key will properly resolve + * + * @param string $key + * + * @return bool + */ + public function hasKey($key) + { + return (!is_null($this->resolve($key))); + } + + /** + * Resolve a key by either returning the appropriate literal or by looking up the appropriate variable + * + * Test for empty has been moved to interpret condition, in Decision + * + * @param string $key + * + * @throws LiquidException + * @return mixed + */ + private function resolve($key) + { + // This shouldn't happen + if (is_array($key)) { + throw new LiquidException("Cannot resolve arrays as key"); + } + + if (is_null($key) || $key == 'null') { + return null; + } + + if ($key == 'true') { + return true; + } + + if ($key == 'false') { + return false; + } + + if (preg_match('/^\'(.*)\'$/', $key, $matches)) { + return $matches[1]; + } + + if (preg_match('/^"(.*)"$/', $key, $matches)) { + return $matches[1]; + } + + if (preg_match('/^(-?\d+)$/', $key, $matches)) { + return $matches[1]; + } + + if (preg_match('/^(-?\d[\d\.]+)$/', $key, $matches)) { + return $matches[1]; + } + + return $this->variable($key); + } + + /** + * Fetches the current key in all the scopes + * + * @param string $key + * + * @return mixed + */ + private function fetch($key) + { + // TagDecrement depends on environments being checked before assigns + foreach ($this->environments as $environment) { + if (array_key_exists($key, $environment)) { + return $environment[$key]; + } + } + + foreach ($this->assigns as $scope) { + if (array_key_exists($key, $scope)) { + $obj = $scope[$key]; + + if ($obj instanceof Drop) { + $obj->setContext($this); + } + + return $obj; + } + } + + return null; + } + + /** + * Resolved the namespaced queries gracefully. + * + * @param string $key + * + * @see Decision::stringValue + * @see AbstractBlock::renderAll + * + * @throws LiquidException + * @return mixed + */ + private function variable($key) + { + // Support numeric and variable array indicies + if (preg_match("|\[[0-9]+\]|", $key)) { + $key = preg_replace("|\[([0-9]+)\]|", ".$1", $key); + } elseif (preg_match("|\[[0-9a-z._]+\]|", $key, $matches)) { + $index = $this->get(str_replace(["[", "]"], "", $matches[0])); + if (strlen($index)) { + $key = preg_replace("|\[([0-9a-z._]+)\]|", ".$index", $key); + } + } + + $parts = explode(Liquid::get('VARIABLE_ATTRIBUTE_SEPARATOR'), $key); + + $object = $this->fetch(array_shift($parts)); + + while (count($parts) > 0) { + // since we still have a part to consider + // and since we can't dig deeper into plain values + // it can be thought as if it has a property with a null value + if (!is_object($object) && !is_array($object) && !is_string($object)) { + return null; + } + + // first try to cast an object to an array or value + if (is_object($object)) { + if (method_exists($object, 'toLiquid')) { + $object = $object->toLiquid(); + } elseif (method_exists($object, 'toArray')) { + $object = $object->toArray(); + } + } + + if (is_null($object)) { + return null; + } + + if ($object instanceof Drop) { + $object->setContext($this); + } + + $nextPartName = array_shift($parts); + + if (is_string($object)) { + if ($nextPartName == 'size') { + // if the last part of the context variable is .size we return the string length + return mb_strlen($object); + } + + // no other special properties for strings, yet + return null; + } + + if (is_array($object)) { + // if the last part of the context variable is .first we return the first array element + if ($nextPartName == 'first' && count($parts) == 0 && !array_key_exists('first', $object)) { + return StandardFilters::first($object); + } + + // if the last part of the context variable is .last we return the last array element + if ($nextPartName == 'last' && count($parts) == 0 && !array_key_exists('last', $object)) { + return StandardFilters::last($object); + } + + // if the last part of the context variable is .size we just return the count + if ($nextPartName == 'size' && count($parts) == 0 && !array_key_exists('size', $object)) { + return count($object); + } + + // no key - no value + if (!array_key_exists($nextPartName, $object)) { + return null; + } + + $object = $object[$nextPartName]; + continue; + } + + if (!is_object($object)) { + // we got plain value, yet asked to resolve a part + // think plain values have a null part with any name + return null; + } + + if ($object instanceof \Countable) { + // if the last part of the context variable is .size we just return the count + if ($nextPartName == 'size' && count($parts) == 0) { + return count($object); + } + } + + if ($object instanceof Drop) { + // if the object is a drop, make sure it supports the given method + if (!$object->hasKey($nextPartName)) { + return null; + } + + $object = $object->invokeDrop($nextPartName); + continue; + } + + // if it has `get` or `field_exists` methods + if (method_exists($object, Liquid::get('HAS_PROPERTY_METHOD'))) { + if (!call_user_func([$object, Liquid::get('HAS_PROPERTY_METHOD')], $nextPartName)) { + return null; + } + + $object = call_user_func([$object, Liquid::get('GET_PROPERTY_METHOD')], $nextPartName); + continue; + } + + // if it's just a regular object, attempt to access a public method + if (is_callable([$object, $nextPartName])) { + $object = call_user_func([$object, $nextPartName]); + continue; + } + + // if a magic accessor method present... + if (is_object($object) && method_exists($object, '__get')) { + $object = $object->$nextPartName; + continue; + } + + // Inexistent property is a null, PHP-speak + if (!property_exists($object, $nextPartName)) { + return null; + } + + // then try a property (independent of accessibility) + if (property_exists($object, $nextPartName)) { + $object = $object->$nextPartName; + continue; + } + + // we'll try casting this object in the next iteration + } + + // lastly, try to get an embedded value of an object + // value could be of any type, not just string, so we have to do this + // conversion here, not later in AbstractBlock::renderAll + if (is_object($object) && method_exists($object, 'toLiquid')) { + $object = $object->toLiquid(); + } + + /* + * Before here were checks for object types and object to string conversion. + * + * Now we just return what we have: + * - Traversable objects are taken care of inside filters + * - Object-to-string conversion is handled at the last moment in Decision::stringValue, and in AbstractBlock::renderAll + * + * This way complex objects could be passed between templates and to filters + */ + + return $object; + } + + public function tick() + { + if ($this->tickFunction === null) { + return; + } + + $tickFunction = $this->tickFunction; + $tickFunction($this); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/CustomFilters.php b/package/vendor/liquid/liquid/src/Liquid/CustomFilters.php new file mode 100644 index 0000000..ce028fc --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/CustomFilters.php @@ -0,0 +1,32 @@ +valid(); + } + + // toLiquid is handled in Context::variable + $class = get_class($value); + throw new RenderException("Value of type $class has no `toLiquid` nor `__toString` methods"); + } + + // Arrays simply return true + if (is_array($value)) { + return $value; + } + + return $value; + } + + /** + * Check to see if to variables are equal in a given context + * + * @param string $left + * @param string $right + * @param Context $context + * + * @return bool + */ + protected function equalVariables($left, $right, Context $context) + { + $left = $this->stringValue($context->get($left)); + $right = $this->stringValue($context->get($right)); + + return ($left == $right); + } + + /** + * Interpret a comparison + * + * @param string $left + * @param string $right + * @param string $op + * @param Context $context + * + * @throws \Liquid\Exception\RenderException + * @return bool + */ + protected function interpretCondition($left, $right, $op, Context $context) + { + if (is_null($op)) { + $value = $this->stringValue($context->get($left)); + return $value; + } + + // values of 'empty' have a special meaning in array comparisons + if ($right == 'empty' && is_array($context->get($left))) { + $left = count($context->get($left)); + $right = 0; + } elseif ($left == 'empty' && is_array($context->get($right))) { + $right = count($context->get($right)); + $left = 0; + } else { + $left = $context->get($left); + $right = $context->get($right); + + $left = $this->stringValue($left); + $right = $this->stringValue($right); + } + + // special rules for null values + if (is_null($left) || is_null($right)) { + // null == null returns true + if ($op == '==' && is_null($left) && is_null($right)) { + return true; + } + + // null != anything other than null return true + if ($op == '!=' && (!is_null($left) || !is_null($right))) { + return true; + } + + // everything else, return false; + return false; + } + + // regular rules + switch ($op) { + case '==': + return ($left == $right); + + case '!=': + return ($left != $right); + + case '>': + return ($left > $right); + + case '<': + return ($left < $right); + + case '>=': + return ($left >= $right); + + case '<=': + return ($left <= $right); + + case 'contains': + return is_array($left) ? in_array($right, $left) : (strpos($left, $right) !== false); + + default: + throw new RenderException("Error in tag '" . $this->name() . "' - Unknown operator $op"); + } + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Document.php b/package/vendor/liquid/liquid/src/Liquid/Document.php new file mode 100644 index 0000000..e2398d9 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Document.php @@ -0,0 +1,95 @@ +fileSystem = $fileSystem; + $this->parse($tokens); + } + + /** + * Check for cached includes; if there are - do not use cache + * + * @see \Liquid\Tag\TagInclude::hasIncludes() + * @see \Liquid\Tag\TagExtends::hasIncludes() + * @return bool if need to discard cache + */ + public function hasIncludes() + { + $seenExtends = false; + $seenBlock = false; + + foreach ($this->nodelist as $token) { + if ($token instanceof TagExtends) { + $seenExtends = true; + } elseif ($token instanceof TagBlock) { + $seenBlock = true; + } + } + + /* + * We try to keep the base templates in cache (that not extend anything). + * + * At the same time if we re-render all other blocks we see, we avoid most + * if not all related caching quirks. This may be suboptimal. + */ + if ($seenBlock && !$seenExtends) { + return true; + } + + foreach ($this->nodelist as $token) { + // check any of the tokens for includes + if ($token instanceof TagInclude && $token->hasIncludes()) { + return true; + } + + if ($token instanceof TagExtends && $token->hasIncludes()) { + return true; + } + } + + return false; + } + + /** + * There isn't a real delimiter + * + * @return string + */ + protected function blockDelimiter() + { + return ''; + } + + /** + * Document blocks don't need to be terminated since they are not actually opened + */ + protected function assertMissingDelimitation() + { + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Drop.php b/package/vendor/liquid/liquid/src/Liquid/Drop.php new file mode 100644 index 0000000..48e71f5 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Drop.php @@ -0,0 +1,107 @@ + 'sales', 'limit' => 10 )); + * } + * } + * + * tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' ) + * tmpl.render('product' => ProductDrop.new ) // will invoke topSales query. + * + * Your drop can either implement the methods sans any parameters or implement the beforeMethod(name) method which is a + * catch all. + */ +abstract class Drop +{ + /** + * @var Context + */ + protected $context; + + /** + * Catch all method that is invoked before a specific method + * + * @param string $method + * + * @return null + */ + protected function beforeMethod($method) + { + return null; + } + + /** + * @param Context $context + */ + public function setContext(Context $context) + { + $this->context = $context; + } + + /** + * Invoke a specific method + * + * @param string $method + * + * @return mixed + */ + public function invokeDrop($method) + { + $result = $this->beforeMethod($method); + + if (is_null($result) && is_callable([$this, $method])) { + $result = $this->$method(); + } + + return $result; + } + + /** + * Returns true if the drop supports the given method + * + * @param string $name + * + * @return bool + */ + public function hasKey($name) + { + return true; + } + + /** + * @return Drop + */ + public function toLiquid() + { + return $this; + } + + /** + * @return string + */ + public function __toString() + { + return get_class($this); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Exception/CacheException.php b/package/vendor/liquid/liquid/src/Liquid/Exception/CacheException.php new file mode 100644 index 0000000..4ccb6b7 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Exception/CacheException.php @@ -0,0 +1,21 @@ +root = $root; + } + + /** + * Retrieve a template file + * + * @param string $templatePath + * + * @return string template content + */ + public function readTemplateFile($templatePath) + { + return file_get_contents($this->fullPath($templatePath)); + } + + /** + * Resolves a given path to a full template file path, making sure it's valid + * + * @param string $templatePath + * + * @throws \Liquid\Exception\ParseException + * @throws \Liquid\Exception\NotFoundException + * @return string + */ + public function fullPath($templatePath) + { + if (empty($templatePath)) { + throw new ParseException("Empty template name"); + } + + $nameRegex = Liquid::get('INCLUDE_ALLOW_EXT') + ? new Regexp('/^[^.\/][a-zA-Z0-9_\.\/-]+$/') + : new Regexp('/^[^.\/][a-zA-Z0-9_\/-]+$/'); + + if (!$nameRegex->match($templatePath)) { + throw new ParseException("Illegal template name '$templatePath'"); + } + + $templateDir = dirname($templatePath); + $templateFile = basename($templatePath); + + if (!Liquid::get('INCLUDE_ALLOW_EXT')) { + $templateFile = Liquid::get('INCLUDE_PREFIX') . $templateFile . '.' . Liquid::get('INCLUDE_SUFFIX'); + } + + $fullPath = join(DIRECTORY_SEPARATOR, [$this->root, $templateDir, $templateFile]); + + $realFullPath = realpath($fullPath); + if ($realFullPath === false) { + throw new NotFoundException("File not found: $fullPath"); + } + + if (strpos($realFullPath, $this->root) !== 0) { + throw new NotFoundException("Illegal template full path: {$realFullPath} not under {$this->root}"); + } + + return $realFullPath; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/FileSystem/Virtual.php b/package/vendor/liquid/liquid/src/Liquid/FileSystem/Virtual.php new file mode 100644 index 0000000..3ceda2f --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/FileSystem/Virtual.php @@ -0,0 +1,64 @@ +callback = $callback; + } + + /** + * Retrieve a template file + * + * @param string $templatePath + * + * @return string template content + */ + public function readTemplateFile($templatePath) + { + return call_user_func($this->callback, $templatePath); + } + + public function __sleep() + { + // we cannot serialize a closure + if ($this->callback instanceof \Closure) { + throw new FilesystemException("Virtual file system with a Closure as a callback cannot be used with a serializing cache"); + } + + return array_keys(get_object_vars($this)); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Filterbank.php b/package/vendor/liquid/liquid/src/Liquid/Filterbank.php new file mode 100644 index 0000000..17c305d --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Filterbank.php @@ -0,0 +1,153 @@ +context = $context; + + $this->addFilter(\Liquid\StandardFilters::class); + $this->addFilter(\Liquid\CustomFilters::class); + } + + /** + * Adds a filter to the bank + * + * @param mixed $filter Can either be an object, the name of a class (in which case the + * filters will be called statically) or the name of a function. + * + * @throws \Liquid\Exception\WrongArgumentException + * @return bool + */ + public function addFilter($filter, ?callable $callback = null) + { + // If it is a callback, save it as it is + if (is_string($filter) && $callback) { + $this->methodMap[$filter] = $callback; + return true; + } + + // If the filter is a class, register all its static methods + if (is_string($filter) && class_exists($filter)) { + $reflection = new \ReflectionClass($filter); + foreach ($reflection->getMethods(\ReflectionMethod::IS_STATIC) as $method) { + $this->methodMap[$method->name] = $method->class; + } + + return true; + } + + // If it's a global function, register it simply + if (is_string($filter) && function_exists($filter)) { + $this->methodMap[$filter] = false; + return true; + } + + // If it isn't an object an isn't a string either, it's a bad parameter + if (!is_object($filter)) { + throw new WrongArgumentException("Parameter passed to addFilter must be an object or a string"); + } + + // If the passed filter was an object, store the object for future reference. + $filter->context = $this->context; + $className = get_class($filter); + $this->filters[$className] = $filter; + + // Then register all public static and not methods as filters + foreach (get_class_methods($filter) as $method) { + if (strtolower($method) === '__construct') { + continue; + } + $this->methodMap[$method] = $className; + } + + return true; + } + + /** + * Invokes the filter with the given name + * + * @param string $name The name of the filter + * @param string $value The value to filter + * @param array $args The additional arguments for the filter + * + * @return string + */ + public function invoke($name, $value, array $args = []) + { + // workaround for a single standard filter being a reserved keyword - we can't use overloading for static calls + if ($name == 'default') { + $name = '_default'; + } + + array_unshift($args, $value); + + // Consult the mapping + if (!isset($this->methodMap[$name])) { + return $value; + } + + $class = $this->methodMap[$name]; + + // If we have a callback + if (is_callable($class)) { + return call_user_func_array($class, $args); + } + + // If we have a registered object for the class, use that instead + if (isset($this->filters[$class])) { + $class = $this->filters[$class]; + } + + // If we're calling a function + if ($class === false) { + return call_user_func_array($name, $args); + } + + // Call a class or an instance method + return call_user_func_array([$class, $name], $args); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Liquid.php b/package/vendor/liquid/liquid/src/Liquid/Liquid.php new file mode 100644 index 0000000..6a02d04 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Liquid.php @@ -0,0 +1,170 @@ + 'field_exists', + + // This method is called on object when resolving variables when + // a given property exists. + 'GET_PROPERTY_METHOD' => 'get', + + // Separator between filters. + 'FILTER_SEPARATOR' => '\|', + + // Separator for arguments. + 'ARGUMENT_SEPARATOR' => ',', + + // Separator for argument names and values. + 'FILTER_ARGUMENT_SEPARATOR' => ':', + + // Separator for variable attributes. + 'VARIABLE_ATTRIBUTE_SEPARATOR' => '.', + + // Allow template names with extension in include and extends tags. + 'INCLUDE_ALLOW_EXT' => false, + + // Suffix for include files. + 'INCLUDE_SUFFIX' => 'liquid', + + // Prefix for include files. + 'INCLUDE_PREFIX' => '_', + + // Whitespace control. + 'WHITESPACE_CONTROL' => '-', + + // Tag start. + 'TAG_START' => '{%', + + // Tag end. + 'TAG_END' => '%}', + + // Variable start. + 'VARIABLE_START' => '{{', + + // Variable end. + 'VARIABLE_END' => '}}', + + // Variable name. + 'VARIABLE_NAME' => '[a-zA-Z_][a-zA-Z_0-9.-]*', + + 'QUOTED_STRING' => '(?:"[^"]*"|\'[^\']*\')', + 'QUOTED_STRING_FILTER_ARGUMENT' => '"[^"]*"|\'[^\']*\'', + + // Automatically escape any variables unless told otherwise by a "raw" filter + 'ESCAPE_BY_DEFAULT' => false, + + // The name of the key to use when building pagination query strings e.g. ?page=1 + 'PAGINATION_REQUEST_KEY' => 'page', + + // The name of the context key used to denote the current page number + 'PAGINATION_CONTEXT_KEY' => 'page', + + // Whenever variables from $_SERVER should be directly available to templates + 'EXPOSE_SERVER' => false, + + // $_SERVER variables whitelist - exposed even when EXPOSE_SERVER is false + 'SERVER_SUPERGLOBAL_WHITELIST' => [ + 'HTTP_ACCEPT', + 'HTTP_ACCEPT_CHARSET', + 'HTTP_ACCEPT_ENCODING', + 'HTTP_ACCEPT_LANGUAGE', + 'HTTP_CONNECTION', + 'HTTP_HOST', + 'HTTP_REFERER', + 'HTTP_USER_AGENT', + 'HTTPS', + 'REQUEST_METHOD', + 'REQUEST_URI', + 'SERVER_NAME', + ], + ]; + + /** + * Get a configuration setting. + * + * @param string $key setting key + * + * @return string + */ + public static function get($key) + { + // backward compatibility + if ($key === 'ALLOWED_VARIABLE_CHARS') { + return substr(self::$config['VARIABLE_NAME'], 0, -1); + } + if (array_key_exists($key, self::$config)) { + return self::$config[$key]; + } + // This case is needed for compound settings + switch ($key) { + case 'QUOTED_FRAGMENT': + return '(?:' . self::get('QUOTED_STRING') . '|[^\s,\|\'"]+)'; + case 'TAG_ATTRIBUTES': + return '/(\w+)\s*\:\s*(' . self::get('QUOTED_FRAGMENT') . ')/'; + case 'TOKENIZATION_REGEXP': + return '/(' . self::$config['TAG_START'] . '.*?' . self::$config['TAG_END'] . '|' . self::$config['VARIABLE_START'] . '.*?' . self::$config['VARIABLE_END'] . ')/s'; + default: + return null; + } + } + + /** + * Changes/creates a setting. + * + * @param string $key + * @param string $value + */ + public static function set($key, $value) + { + // backward compatibility + if ($key === 'ALLOWED_VARIABLE_CHARS') { + $key = 'VARIABLE_NAME'; + $value .= '+'; + } + self::$config[$key] = $value; + } + + /** + * Flatten a multidimensional array into a single array. Does not maintain keys. + * + * @param array $array + * + * @return array + */ + public static function arrayFlatten($array) + { + $return = []; + + foreach ($array as $element) { + if (is_array($element)) { + $return = array_merge($return, self::arrayFlatten($element)); + } else { + $return[] = $element; + } + } + return $return; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/LiquidException.php b/package/vendor/liquid/liquid/src/Liquid/LiquidException.php new file mode 100644 index 0000000..402c69d --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/LiquidException.php @@ -0,0 +1,19 @@ +pattern = (substr($pattern, 0, 1) != '/') + ? '/' . $this->quote($pattern) . '/' + : $pattern; + } + + /** + * Quotes regular expression characters + * + * @param string $string + * + * @return string + */ + public function quote($string) + { + return preg_quote($string, '/'); + } + + /** + * Returns an array of matches for the string in the same way as Ruby's scan method + * + * @param string $string + * + * @return array + */ + public function scan($string) + { + preg_match_all($this->pattern, $string, $matches); + + if (count($matches) == 1) { + return $matches[0]; + } + + array_shift($matches); + + $result = []; + + foreach ($matches as $matchKey => $subMatches) { + foreach ($subMatches as $subMatchKey => $subMatch) { + $result[$subMatchKey][$matchKey] = $subMatch; + } + } + + return $result; + } + + /** + * Matches the given string. Only matches once. + * + * @param string $string + * + * @return int 1 if there was a match, 0 if there wasn't + */ + public function match($string) + { + return preg_match($this->pattern, $string, $this->matches); + } + + /** + * Matches the given string. Matches all. + * + * @param string $string + * + * @return int The number of matches + */ + public function matchAll($string) + { + return preg_match_all($this->pattern, $string, $this->matches); + } + + /** + * Splits the given string + * + * @param string $string + * @param int $limit Limits the amount of results returned + * + * @return array + */ + public function split($string, $limit = -1) + { + return preg_split($this->pattern, $string, $limit); + } + + /** + * Returns the original pattern primarily for debugging purposes + * + * @return string + */ + public function __toString() + { + return $this->pattern; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/StandardFilters.php b/package/vendor/liquid/liquid/src/Liquid/StandardFilters.php new file mode 100644 index 0000000..086b517 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/StandardFilters.php @@ -0,0 +1,785 @@ +format($dateFormat); + + return $formatted; + } + + + /** + * Default + * + * @param string $input + * @param string $default_value + * + * @return string + */ + public static function _default($input, $default_value) + { + $isBlank = $input == '' || $input === false || $input === null; + return $isBlank ? $default_value : $input; + } + + + /** + * division + * + * @param float $input + * @param float $operand + * + * @return float + */ + public static function divided_by($input, $operand) + { + return (float)$input / (float)$operand; + } + + + /** + * Convert an input to lowercase + * + * @param string $input + * + * @return string + */ + public static function downcase($input) + { + return is_string($input) ? mb_strtolower($input) : $input; + } + + + /** + * Pseudo-filter: negates auto-added escape filter + * + * @param string $input + * + * @return string + */ + public static function raw($input) + { + return $input; + } + + + /** + * Converts into JSON string + * + * @param mixed $input + * + * @return string + */ + public static function json($input) + { + return json_encode($input); + } + + /** + * Creates an array including only the objects with a given property value + * @link https://shopify.github.io/liquid/filters/where/ + * + * @param mixed $input + * @param string ...$args + * + * @throws LiquidException + * @return mixed + */ + public static function where($input, string ...$args) + { + if (is_array($input)) { + switch (count($args)) { + case 1: + return array_values(array_filter($input, fn ($v) => !in_array($v[$args[0]] ?? null, [null, false], true))); + case 2: + return array_values(array_filter($input, fn ($v) => ($v[$args[0]] ?? '') == $args[1])); + default: + throw new LiquidException('Wrong number of arguments to function `where`, given ' . count($args) . ', expected 1 or 2'); + } + } + return $input; + } + + /** + * Escape a string + * + * @param string $input + * + * @return string + */ + public static function escape($input) + { + // Arrays are taken care down the stack with an error + if (is_array($input)) { + return $input; + } + + if (is_null($input)) { + return ''; + } + + return htmlentities($input, ENT_QUOTES); + } + + + /** + * Escape a string once, keeping all previous HTML entities intact + * + * @param string $input + * + * @return string + */ + public static function escape_once($input) + { + // Arrays are taken care down the stack with an error + if (is_array($input)) { + return $input; + } + + return htmlentities($input, ENT_QUOTES, null, false); + } + + + /** + * Returns the first element of an array + * + * @param array|\Iterator $input + * + * @return mixed + */ + public static function first($input) + { + if ($input instanceof \Iterator) { + $input->rewind(); + return $input->current(); + } + return is_array($input) ? reset($input) : $input; + } + + + /** + * @param mixed $input number + * + * @return int + */ + public static function floor($input) + { + return (int) floor((float)$input); + } + + + /** + * Joins elements of an array with a given character between them + * + * @param array|\Traversable $input + * @param string $glue + * + * @return string + */ + public static function join($input, $glue = ' ') + { + if ($input instanceof \Traversable) { + $str = ''; + foreach ($input as $elem) { + if ($str) { + $str .= $glue; + } + $str .= $elem; + } + return $str; + } + return is_array($input) ? implode($glue, $input) : $input; + } + + + /** + * Returns the last element of an array + * + * @param array|\Traversable $input + * + * @return mixed + */ + public static function last($input) + { + if ($input instanceof \Traversable) { + $last = null; + foreach ($input as $elem) { + $last = $elem; + } + return $last; + } + return is_array($input) ? end($input) : $input; + } + + + /** + * @param string $input + * + * @return string + */ + public static function lstrip($input) + { + return ltrim($input); + } + + + /** + * Map/collect on a given property + * + * @param array|\Traversable $input + * @param string $property + * + * @return string + */ + public static function map($input, $property) + { + if ($input instanceof \Traversable) { + $input = iterator_to_array($input); + } + if (!is_array($input)) { + return $input; + } + return array_map(function ($elem) use ($property) { + if (is_callable($elem)) { + return $elem(); + } elseif (is_array($elem) && array_key_exists($property, $elem)) { + return $elem[$property]; + } + return null; + }, $input); + } + + + /** + * subtraction + * + * @param float $input + * @param float $operand + * + * @return float + */ + public static function minus($input, $operand) + { + return (float)$input - (float)$operand; + } + + + /** + * modulo + * + * @param float $input + * @param float $operand + * + * @return float + */ + public static function modulo($input, $operand) + { + return fmod((float)$input, (float)$operand); + } + + + /** + * Replace each newline (\n) with html break + * + * @param string $input + * + * @return string + */ + public static function newline_to_br($input) + { + return is_string($input) ? str_replace("\n", "
\n", $input) : $input; + } + + + /** + * addition + * + * @param float $input + * @param float $operand + * + * @return float + */ + public static function plus($input, $operand) + { + return (float)$input + (float)$operand; + } + + + /** + * Prepend a string to another + * + * @param string $input + * @param string $string + * + * @return string + */ + public static function prepend($input, $string) + { + return $string . $input; + } + + + /** + * Remove a substring + * + * @param string $input + * @param string $string + * + * @return string + */ + public static function remove($input, $string) + { + return str_replace($string, '', $input); + } + + + /** + * Remove the first occurrences of a substring + * + * @param string $input + * @param string $string + * + * @return string + */ + public static function remove_first($input, $string) + { + if (($pos = strpos($input, $string)) !== false) { + $input = substr_replace($input, '', $pos, strlen($string)); + } + + return $input; + } + + + /** + * Replace occurrences of a string with another + * + * @param string $input + * @param string $string + * @param string $replacement + * + * @return string + */ + public static function replace($input, $string, $replacement = '') + { + return str_replace($string, $replacement, $input); + } + + + /** + * Replace the first occurrences of a string with another + * + * @param string $input + * @param string $string + * @param string $replacement + * + * @return string + */ + public static function replace_first($input, $string, $replacement = '') + { + if (($pos = strpos($input, $string)) !== false) { + $input = substr_replace($input, $replacement, $pos, strlen($string)); + } + + return $input; + } + + + /** + * Reverse the elements of an array + * + * @param array|\Traversable $input + * + * @return array + */ + public static function reverse($input) + { + if ($input instanceof \Traversable) { + $input = iterator_to_array($input); + } + return array_reverse($input); + } + + + /** + * Round a number + * + * @param float $input + * @param int $n precision + * + * @return float + */ + public static function round($input, $n = 0) + { + return round((float)$input, (int)$n); + } + + + /** + * @param string $input + * + * @return string + */ + public static function rstrip($input) + { + return rtrim($input); + } + + + /** + * Return the size of an array or of an string + * + * @param mixed $input + * @throws RenderException + * @return int + */ + public static function size($input) + { + if ($input instanceof \Iterator) { + return iterator_count($input); + } + + if (is_array($input)) { + return count($input); + } + + if (is_object($input)) { + if (method_exists($input, 'size')) { + return $input->size(); + } + + if (!method_exists($input, '__toString')) { + $class = get_class($input); + throw new RenderException("Size of $class cannot be estimated: it has no method 'size' nor can be converted to a string"); + } + } + + // only plain values and stringable objects left at this point + return strlen($input); + } + + + /** + * @param array|\Iterator|string $input + * @param int $offset + * @param int $length + * + * @return array|\Iterator|string + */ + public static function slice($input, $offset, $length = null) + { + if ($input instanceof \Iterator) { + $input = iterator_to_array($input); + } + if (is_array($input)) { + $input = array_slice($input, $offset, $length); + } elseif (is_string($input)) { + $input = mb_substr($input, $offset, $length); + } + + return $input; + } + + + /** + * Sort the elements of an array + * + * @param array|\Traversable $input + * @param string $property use this property of an array element + * + * @return array + */ + public static function sort($input, $property = null) + { + if ($input instanceof \Traversable) { + $input = iterator_to_array($input); + } + if ($property === null) { + asort($input); + } else { + $first = reset($input); + if ($first !== false && is_array($first) && array_key_exists($property, $first)) { + uasort($input, function ($a, $b) use ($property) { + if (($a[$property] ?? 0) == ($b[$property] ?? 0)) { + return 0; + } + + return ($a[$property] ?? 0) < ($b[$property] ?? 0) ? -1 : 1; + }); + } + } + + return $input; + } + + /** + * Explicit string conversion. + * + * @param mixed $input + * + * @return string + */ + public static function string($input) + { + return strval($input); + } + + /** + * Split input string into an array of substrings separated by given pattern. + * + * @param string $input + * @param string $pattern + * + * @return array + */ + public static function split($input, $pattern) + { + if ($input === '' || $input === null) { + return []; + } + + if ($pattern === '') { + return mb_str_split($input); + } + + return explode($pattern, $input); + } + + + /** + * @param string $input + * + * @return string + */ + public static function strip($input) + { + return trim($input); + } + + + /** + * Removes html tags from text + * + * @param string $input + * + * @return string + */ + public static function strip_html($input) + { + return is_string($input) ? strip_tags($input) : $input; + } + + + /** + * Strip all newlines (\n, \r) from string + * + * @param string $input + * + * @return string + */ + public static function strip_newlines($input) + { + return is_string($input) ? str_replace([ + "\n", "\r", + ], '', $input) : $input; + } + + + /** + * multiplication + * + * @param float $input + * @param float $operand + * + * @return float + */ + public static function times($input, $operand) + { + return (float)$input * (float)$operand; + } + + + /** + * Truncate a string down to x characters + * + * @param string $input + * @param int $characters + * @param string $ending string to append if truncated + * + * @return string + */ + public static function truncate($input, $characters = 100, $ending = '...') + { + if (is_string($input) || is_numeric($input)) { + if (strlen($input) > $characters) { + return mb_substr($input, 0, $characters) . $ending; + } + } + + return $input; + } + + + /** + * Truncate string down to x words + * + * @param string $input + * @param int $words + * @param string $ending string to append if truncated + * + * @return string + */ + public static function truncatewords($input, $words = 3, $ending = '...') + { + if (is_string($input)) { + $wordlist = explode(" ", $input); + + if (count($wordlist) > $words) { + return implode(" ", array_slice($wordlist, 0, $words)) . $ending; + } + } + + return $input; + } + + + /** + * Remove duplicate elements from an array + * + * @param array|\Traversable $input + * + * @return array + */ + public static function uniq($input) + { + if ($input instanceof \Traversable) { + $input = iterator_to_array($input); + } + return array_unique($input); + } + + + /** + * Convert an input to uppercase + * + * @param string $input + * + * @return string + */ + public static function upcase($input) + { + return is_string($input) ? mb_strtoupper($input) : $input; + } + + + /** + * URL encodes a string + * + * @param string $input + * + * @return string + */ + public static function url_encode($input) + { + return urlencode($input); + } + + /** + * Decodes a URL-encoded string + * + * @param string $input + * + * @return string + */ + public static function url_decode($input) + { + return urldecode($input); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagAssign.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagAssign.php new file mode 100644 index 0000000..d47710f --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagAssign.php @@ -0,0 +1,76 @@ +match($markup)) { + $this->to = $syntaxRegexp->matches[1]; + $this->from = new Variable($syntaxRegexp->matches[2]); + } else { + throw new ParseException("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"); + } + } + + /** + * Renders the tag + * + * @param Context $context + * + * @return string|void + */ + public function render(Context $context) + { + $output = $this->from->render($context); + + $context->set($this->to, $output, true); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagBlock.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagBlock.php new file mode 100644 index 0000000..188e13b --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagBlock.php @@ -0,0 +1,56 @@ +match($markup)) { + $this->block = $syntaxRegexp->matches[1]; + parent::__construct($markup, $tokens, $fileSystem); + } else { + throw new ParseException("Syntax Error in 'block' - Valid syntax: block [name]"); + } + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagBreak.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagBreak.php new file mode 100644 index 0000000..5a16828 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagBreak.php @@ -0,0 +1,42 @@ +registers['break'] = true; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagCapture.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagCapture.php new file mode 100644 index 0000000..afcd1a0 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagCapture.php @@ -0,0 +1,71 @@ +match($markup)) { + $this->to = $syntaxRegexp->matches[1]; + parent::__construct($markup, $tokens, $fileSystem); + } else { + throw new ParseException("Syntax Error in 'capture' - Valid syntax: capture [var] [value]"); + } + } + + /** + * Renders the block + * + * @param Context $context + * + * @return string + */ + public function render(Context $context) + { + $output = parent::render($context); + + $context->set($this->to, $output, true); + return ''; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagCase.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagCase.php new file mode 100644 index 0000000..b2f4d2a --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagCase.php @@ -0,0 +1,174 @@ +nodelists = []; + $this->elseNodelist = []; + + parent::__construct($markup, $tokens, $fileSystem); + + $syntaxRegexp = new Regexp('/' . Liquid::get('QUOTED_FRAGMENT') . '/'); + + if ($syntaxRegexp->match($markup)) { + $this->left = $syntaxRegexp->matches[0]; + } else { + throw new ParseException("Syntax Error in tag 'case' - Valid syntax: case [condition]"); // harry + } + } + + /** + * Pushes the last nodelist onto the stack + */ + public function endTag() + { + $this->pushNodelist(); + } + + /** + * Unknown tag handler + * + * @param string $tag + * @param string $params + * @param array $tokens + * + * @throws \Liquid\Exception\ParseException + */ + public function unknownTag($tag, $params, array $tokens) + { + switch ($tag) { + case 'when': + $whenSyntax = preg_match_all('/(?<=,|or|^)\s*(' . Liquid::get('QUOTED_FRAGMENT') . ')/', $params, $matches); + // push the current nodelist onto the stack and prepare for a new one + if ($whenSyntax) { + $this->pushNodelist(); + $this->right = $matches[1]; + $this->nodelist = []; + } else { + throw new ParseException("Syntax Error in tag 'case' - Valid when condition: when [condition]"); // harry + } + break; + + case 'else': + // push the last nodelist onto the stack and prepare to receive the else nodes + $this->pushNodelist(); + $this->right = null; + $this->elseNodelist = &$this->nodelist; + $this->nodelist = []; + break; + + default: + parent::unknownTag($tag, $params, $tokens); + } + } + + /** + * Pushes the current right value and nodelist into the nodelist stack + */ + public function pushNodelist() + { + if (!is_null($this->right)) { + $this->nodelists[] = [$this->right, $this->nodelist]; + } + } + + /** + * Renders the node + * + * @param Context $context + * + * @return string + */ + public function render(Context $context) + { + $output = ''; // array(); + $runElseBlock = true; + + foreach ($this->nodelists as $data) { + list($right, $nodelist) = $data; + + foreach ($right as $var) { + if ($this->equalVariables($this->left, $var, $context)) { + $runElseBlock = false; + + $context->push(); + $output .= $this->renderAll($nodelist, $context); + $context->pop(); + + break; + } + } + } + + if ($runElseBlock) { + $context->push(); + $output .= $this->renderAll($this->elseNodelist, $context); + $context->pop(); + } + + return $output; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagComment.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagComment.php new file mode 100644 index 0000000..e41cc06 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagComment.php @@ -0,0 +1,37 @@ +registers['continue'] = true; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagCycle.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagCycle.php new file mode 100644 index 0000000..dd7e88d --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagCycle.php @@ -0,0 +1,130 @@ +match($markup)) { + $this->variables = $this->variablesFromString($namedSyntax->matches[2]); + $this->name = $namedSyntax->matches[1]; + } elseif ($simpleSyntax->match($markup)) { + $this->variables = $this->variablesFromString($markup); + $this->name = "'" . implode($this->variables) . "'"; + } else { + throw new ParseException("Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"); + } + } + + /** + * Renders the tag + * + * @var Context $context + * @return string + */ + public function render(Context $context) + { + $context->push(); + + $key = $context->get($this->name); + + if (isset($context->registers['cycle'][$key])) { + $iteration = $context->registers['cycle'][$key]; + } else { + $iteration = 0; + } + + $result = $context->get($this->variables[$iteration]); + + $iteration += 1; + + if ($iteration >= count($this->variables)) { + $iteration = 0; + } + + $context->registers['cycle'][$key] = $iteration; + + $context->pop(); + + return $result; + } + + /** + * Extract variables from a string of markup + * + * @param string $markup + * + * @return array; + */ + private function variablesFromString($markup) + { + $regexp = new Regexp('/\s*(' . Liquid::get('QUOTED_FRAGMENT') . ')\s*/'); + $parts = explode(',', $markup); + $result = []; + + foreach ($parts as $part) { + $regexp->match($part); + + if (!empty($regexp->matches[1])) { + $result[] = $regexp->matches[1]; + } + } + + return $result; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagDecrement.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagDecrement.php new file mode 100644 index 0000000..dde684e --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagDecrement.php @@ -0,0 +1,83 @@ +match($markup)) { + $this->toDecrement = $syntax->matches[0]; + } else { + throw new ParseException("Syntax Error in 'decrement' - Valid syntax: decrement [var]"); + } + } + + /** + * Renders the tag + * + * @param Context $context + * + * @return string|void + */ + public function render(Context $context) + { + // if the value is not set in the environment check to see if it + // exists in the context, and if not set it to 0 + if (!isset($context->environments[0][$this->toDecrement])) { + // check for a context value + $fromContext = $context->get($this->toDecrement); + + // we already have a value in the context + $context->environments[0][$this->toDecrement] = (null !== $fromContext) ? $fromContext : 0; + } + + // decrement the environment value + $context->environments[0][$this->toDecrement]--; + + return ''; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagExtends.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagExtends.php new file mode 100644 index 0000000..417d9fd --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagExtends.php @@ -0,0 +1,220 @@ +match($markup) && isset($regex->matches[1])) { + $this->templateName = substr($regex->matches[1], 1, strlen($regex->matches[1]) - 2); + } else { + throw new ParseException("Error in tag 'extends' - Valid syntax: extends '[template name]'"); + } + + parent::__construct($markup, $tokens, $fileSystem); + } + + /** + * @param array $tokens + * + * @return array + */ + private function findBlocks(array $tokens) + { + $blockstartRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*block (\w+)\s*(.*)?' . Liquid::get('TAG_END') . '$/'); + $blockendRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*endblock\s*?' . Liquid::get('TAG_END') . '$/'); + + $b = []; + $name = null; + + for ($i = 0, $n = count($tokens); $i < $n; $i++) { + if ($tokens[$i] === null) { + continue; + } + $token = $tokens[$i]; + $tokens[$i] = null; + + if ($blockstartRegexp->match($token)) { + $name = $blockstartRegexp->matches[1]; + $b[$name] = []; + } elseif ($blockendRegexp->match($token)) { + $name = null; + } else { + if ($name !== null) { + array_push($b[$name], $token); + } + } + } + + return $b; + } + + /** + * Parses the tokens + * + * @param array $tokens + * + * @throws \Liquid\Exception\MissingFilesystemException + */ + public function parse(array &$tokens) + { + if ($this->fileSystem === null) { + throw new MissingFilesystemException("No file system"); + } + + // read the source of the template and create a new sub document + $source = $this->fileSystem->readTemplateFile($this->templateName); + + // tokens in this new document + $maintokens = Template::tokenize($source); + + $eRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*extends (.*)?' . Liquid::get('TAG_END') . '$/'); + foreach ($maintokens as $maintoken) { + if ($eRegexp->match($maintoken)) { + $m = $eRegexp->matches[1]; + break; + } + } + + if (isset($m)) { + $rest = array_merge($maintokens, $tokens); + } else { + $childtokens = $this->findBlocks($tokens); + + $blockstartRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*block (\w+)\s*(.*)?' . Liquid::get('TAG_END') . '$/'); + $blockendRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*endblock\s*?' . Liquid::get('TAG_END') . '$/'); + + $name = null; + + $rest = []; + $keep = false; + + for ($i = 0; $i < count($maintokens); $i++) { + if ($blockstartRegexp->match($maintokens[$i])) { + $name = $blockstartRegexp->matches[1]; + + if (isset($childtokens[$name])) { + $keep = true; + array_push($rest, $maintokens[$i]); + foreach ($childtokens[$name] as $item) { + array_push($rest, $item); + } + } + } + if (!$keep) { + array_push($rest, $maintokens[$i]); + } + + if ($blockendRegexp->match($maintokens[$i]) && $keep === true) { + $keep = false; + array_push($rest, $maintokens[$i]); + } + } + } + + $cache = Template::getCache(); + + if (!$cache) { + $this->document = new Document($rest, $this->fileSystem); + return; + } + + $this->hash = md5($source); + + $this->document = $cache->read($this->hash); + + if ($this->document == false || $this->document->hasIncludes() == true) { + $this->document = new Document($rest, $this->fileSystem); + $cache->write($this->hash, $this->document); + } + } + + /** + * Check for cached includes; if there are - do not use cache + * + * @see Document::hasIncludes() + * @return boolean + */ + public function hasIncludes() + { + if ($this->document->hasIncludes() == true) { + return true; + } + + $source = $this->fileSystem->readTemplateFile($this->templateName); + + if (Template::getCache()->exists(md5($source)) && $this->hash === md5($source)) { + return false; + } + + return true; + } + + /** + * Renders the node + * + * @param Context $context + * + * @return string + */ + public function render(Context $context) + { + $context->push(); + $result = $this->document->render($context); + $context->pop(); + return $result; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagFor.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagFor.php new file mode 100644 index 0000000..9b616ac --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagFor.php @@ -0,0 +1,241 @@ +match($markup)) { + $this->variableName = $syntaxRegexp->matches[1]; + $this->collectionName = $syntaxRegexp->matches[2]; + $this->name = $syntaxRegexp->matches[1] . '-' . $syntaxRegexp->matches[2]; + $this->extractAttributes($markup); + } else { + $syntaxRegexp = new Regexp('/(\w+)\s+in\s+\((\d+|' . Liquid::get('VARIABLE_NAME') . ')\s*\.\.\s*(\d+|' . Liquid::get('VARIABLE_NAME') . ')\)/'); + if ($syntaxRegexp->match($markup)) { + $this->type = 'digit'; + $this->variableName = $syntaxRegexp->matches[1]; + $this->start = $syntaxRegexp->matches[2]; + $this->collectionName = $syntaxRegexp->matches[3]; + $this->name = $syntaxRegexp->matches[1].'-digit'; + $this->extractAttributes($markup); + } else { + throw new ParseException("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"); + } + } + } + + /** + * Renders the tag + * + * @param Context $context + * + * @return null|string + */ + public function render(Context $context) + { + if (!isset($context->registers['for'])) { + $context->registers['for'] = []; + } + + if ($this->type == 'digit') { + return $this->renderDigit($context); + } + + // that's the default + return $this->renderCollection($context); + } + + private function renderCollection(Context $context) + { + $collection = $context->get($this->collectionName); + + if ($collection instanceof \Generator && !$collection->valid()) { + return ''; + } + + if ($collection instanceof \Traversable) { + $collection = iterator_to_array($collection); + } + + if (is_null($collection) || !is_array($collection) || count($collection) == 0) { + return ''; + } + + $range = [0, count($collection)]; + + if (isset($this->attributes['limit']) || isset($this->attributes['offset'])) { + $offset = 0; + + if (isset($this->attributes['offset'])) { + $offset = ($this->attributes['offset'] == 'continue') ? $context->registers['for'][$this->name] : $context->get($this->attributes['offset']); + } + + $limit = (isset($this->attributes['limit'])) ? $context->get($this->attributes['limit']) : null; + $rangeEnd = $limit ? $limit : count($collection) - $offset; + $range = [$offset, $rangeEnd]; + + $context->registers['for'][$this->name] = $rangeEnd + $offset; + } + + $result = ''; + $segment = array_slice($collection, $range[0], $range[1]); + if (!count($segment)) { + return null; + } + + $context->push(); + $length = count($segment); + + $index = 0; + foreach ($segment as $key => $item) { + $value = is_numeric($key) ? $item : [$key, $item]; + $context->set($this->variableName, $value); + $context->set('forloop', [ + 'name' => $this->name, + 'length' => $length, + 'index' => $index + 1, + 'index0' => $index, + 'rindex' => $length - $index, + 'rindex0' => $length - $index - 1, + 'first' => (int)($index == 0), + 'last' => (int)($index == $length - 1), + ]); + + $result .= $this->renderAll($this->nodelist, $context); + + $index++; + + if (isset($context->registers['break'])) { + unset($context->registers['break']); + break; + } + if (isset($context->registers['continue'])) { + unset($context->registers['continue']); + } + } + + $context->pop(); + + return $result; + } + + private function renderDigit(Context $context) + { + $start = $this->start; + if (!is_integer($this->start)) { + $start = $context->get($this->start); + } + + $end = $this->collectionName; + if (!is_integer($this->collectionName)) { + $end = $context->get($this->collectionName); + } + + $range = [$start, $end]; + + $context->push(); + $result = ''; + $index = 0; + $length = $range[1] - $range[0]; + for ($i = $range[0]; $i <= $range[1]; $i++) { + $context->set($this->variableName, $i); + $context->set('forloop', [ + 'name' => $this->name, + 'length' => $length, + 'index' => $index + 1, + 'index0' => $index, + 'rindex' => $length - $index, + 'rindex0' => $length - $index - 1, + 'first' => (int)($index == 0), + 'last' => (int)($index == $length - 1), + ]); + + $result .= $this->renderAll($this->nodelist, $context); + + $index++; + + if (isset($context->registers['break'])) { + unset($context->registers['break']); + break; + } + if (isset($context->registers['continue'])) { + unset($context->registers['continue']); + } + } + + $context->pop(); + + return $result; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagIf.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagIf.php new file mode 100644 index 0000000..4624ae4 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagIf.php @@ -0,0 +1,168 @@ +nodelist = & $this->nodelistHolders[count($this->blocks)]; + + array_push($this->blocks, ['if', $markup, &$this->nodelist]); + + parent::__construct($markup, $tokens, $fileSystem); + } + + /** + * Handler for unknown tags, handle else tags + * + * @param string $tag + * @param array $params + * @param array $tokens + */ + public function unknownTag($tag, $params, array $tokens) + { + if ($tag == 'else' || $tag == 'elsif') { + // Update reference to nodelistHolder for this block + $this->nodelist = & $this->nodelistHolders[count($this->blocks) + 1]; + $this->nodelistHolders[count($this->blocks) + 1] = []; + + array_push($this->blocks, [$tag, $params, &$this->nodelist]); + } else { + parent::unknownTag($tag, $params, $tokens); + } + } + + /** + * Render the tag + * + * @param Context $context + * + * @throws \Liquid\Exception\ParseException + * @return string + */ + public function render(Context $context) + { + $context->push(); + + $logicalRegex = new Regexp('/\s+(and|or)\s+/'); + $conditionalRegex = new Regexp('/(' . Liquid::get('QUOTED_FRAGMENT') . ')\s*([=!<>a-z_]+)?\s*(' . Liquid::get('QUOTED_FRAGMENT') . ')?/'); + + $result = ''; + foreach ($this->blocks as $block) { + if ($block[0] == 'else') { + $result = $this->renderAll($block[2], $context); + + break; + } + + if ($block[0] == 'if' || $block[0] == 'elsif') { + // Extract logical operators + $logicalRegex->matchAll($block[1]); + + $logicalOperators = $logicalRegex->matches; + $logicalOperators = $logicalOperators[1]; + // Extract individual conditions + $temp = $logicalRegex->split($block[1]); + + $conditions = []; + + foreach ($temp as $condition) { + if ($conditionalRegex->match($condition)) { + $left = (isset($conditionalRegex->matches[1])) ? $conditionalRegex->matches[1] : null; + $operator = (isset($conditionalRegex->matches[2])) ? $conditionalRegex->matches[2] : null; + $right = (isset($conditionalRegex->matches[3])) ? $conditionalRegex->matches[3] : null; + + array_push($conditions, [ + 'left' => $left, + 'operator' => $operator, + 'right' => $right, + ]); + } else { + throw new ParseException("Syntax Error in tag 'if' - Valid syntax: if [condition]"); + } + } + if (count($logicalOperators)) { + // If statement contains and/or + $display = $this->interpretCondition($conditions[0]['left'], $conditions[0]['right'], $conditions[0]['operator'], $context); + foreach ($logicalOperators as $k => $logicalOperator) { + if ($logicalOperator == 'and') { + $display = ($display && $this->interpretCondition($conditions[$k + 1]['left'], $conditions[$k + 1]['right'], $conditions[$k + 1]['operator'], $context)); + } else { + $display = ($display || $this->interpretCondition($conditions[$k + 1]['left'], $conditions[$k + 1]['right'], $conditions[$k + 1]['operator'], $context)); + } + } + } else { + // If statement is a single condition + $display = $this->interpretCondition($conditions[0]['left'], $conditions[0]['right'], $conditions[0]['operator'], $context); + } + + // hook for unless tag + $display = $this->negateIfUnless($display); + + if ($display) { + $result = $this->renderAll($block[2], $context); + + break; + } + } + } + + $context->pop(); + + return $result; + } + + protected function negateIfUnless($display) + { + // no need to negate a condition in a regular `if` tag (will do that in `unless` tag) + return $display; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagIfchanged.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagIfchanged.php new file mode 100644 index 0000000..42eb371 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagIfchanged.php @@ -0,0 +1,61 @@ +lastValue == $output) { + return ''; + } + $this->lastValue = $output; + return $this->lastValue; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagInclude.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagInclude.php new file mode 100644 index 0000000..fe03d9b --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagInclude.php @@ -0,0 +1,200 @@ +match($markup)) { + throw new ParseException("Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"); + } + + $unquoted = (strpos($regex->matches[1], '"') === false && strpos($regex->matches[1], "'") === false); + + $start = 1; + $len = strlen($regex->matches[1]) - 2; + + if ($unquoted) { + $start = 0; + $len = strlen($regex->matches[1]); + } + + $this->templateName = substr($regex->matches[1], $start, $len); + + if (isset($regex->matches[1])) { + $this->collection = (isset($regex->matches[3])) ? ($regex->matches[3] == "for") : null; + $this->variable = (isset($regex->matches[4])) ? $regex->matches[4] : null; + } + + $this->extractAttributes($markup); + + parent::__construct($markup, $tokens, $fileSystem); + } + + /** + * Parses the tokens + * + * @param array $tokens + * + * @throws \Liquid\Exception\MissingFilesystemException + */ + public function parse(array &$tokens) + { + if ($this->fileSystem === null) { + throw new MissingFilesystemException("No file system"); + } + + // read the source of the template and create a new sub document + $source = $this->fileSystem->readTemplateFile($this->templateName); + + $cache = Template::getCache(); + + if (!$cache) { + // tokens in this new document + $templateTokens = Template::tokenize($source); + $this->document = new Document($templateTokens, $this->fileSystem); + return; + } + + $this->hash = md5($source); + $this->document = $cache->read($this->hash); + + if ($this->document == false || $this->document->hasIncludes() == true) { + $templateTokens = Template::tokenize($source); + $this->document = new Document($templateTokens, $this->fileSystem); + $cache->write($this->hash, $this->document); + } + } + + /** + * Check for cached includes; if there are - do not use cache + * + * @see Document::hasIncludes() + * @return boolean + */ + public function hasIncludes() + { + if ($this->document->hasIncludes() == true) { + return true; + } + + $source = $this->fileSystem->readTemplateFile($this->templateName); + + if (Template::getCache()->exists(md5($source)) && $this->hash === md5($source)) { + return false; + } + + return true; + } + + /** + * Renders the node + * + * @param Context $context + * + * @return string + */ + public function render(Context $context) + { + $result = ''; + $variable = $context->get($this->variable); + + $context->push(); + + foreach ($this->attributes as $key => $value) { + $context->set($key, $context->get($value)); + } + + if ($this->collection) { + foreach ($variable as $item) { + $context->set($this->templateName, $item); + $result .= $this->document->render($context); + } + } else { + if (!is_null($this->variable)) { + $context->set($this->templateName, $variable); + } + + $result .= $this->document->render($context); + } + + $context->pop(); + + return $result; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagIncrement.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagIncrement.php new file mode 100644 index 0000000..91fe0f8 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagIncrement.php @@ -0,0 +1,83 @@ +match($markup)) { + $this->toIncrement = $syntax->matches[0]; + } else { + throw new ParseException("Syntax Error in 'increment' - Valid syntax: increment [var]"); + } + } + + /** + * Renders the tag + * + * @param Context $context + * + * @return string|void + */ + public function render(Context $context) + { + // If the value is not set in the environment check to see if it + // exists in the context, and if not set it to -1 + if (!isset($context->environments[0][$this->toIncrement])) { + // check for a context value + $from_context = $context->get($this->toIncrement); + + // we already have a value in the context + $context->environments[0][$this->toIncrement] = (null !== $from_context) ? $from_context : -1; + } + + // Increment the value + $context->environments[0][$this->toIncrement]++; + + return ''; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagPaginate.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagPaginate.php new file mode 100644 index 0000000..3d1fc3c --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagPaginate.php @@ -0,0 +1,206 @@ + + * {% endfor %} + * {% endpaginate %} + * + */ + +class TagPaginate extends AbstractBlock +{ + /** + * @var array The collection to paginate + */ + private $collectionName; + + /** + * @var array The collection object + */ + private $collection; + + /** + * @var int The size of the collection + */ + private $collectionSize; + + /** + * @var int The number of items to paginate by + */ + private $numberItems; + + /** + * @var int The current page + */ + private $currentPage; + + /** + * @var int The current offset (no of pages times no of items) + */ + private $currentOffset; + + /** + * @var int Total pages + */ + private $totalPages; + + + /** + * Constructor + * + * @param string $markup + * @param array $tokens + * @param FileSystem|null $fileSystem + * + * @throws \Liquid\Exception\ParseException + * + */ + public function __construct($markup, array &$tokens, ?FileSystem $fileSystem = null) + { + parent::__construct($markup, $tokens, $fileSystem); + + $syntax = new Regexp('/(' . Liquid::get('VARIABLE_NAME') . ')\s+by\s+(\w+)/'); + + if ($syntax->match($markup)) { + $this->collectionName = $syntax->matches[1]; + $this->numberItems = $syntax->matches[2]; + $this->extractAttributes($markup); + } else { + throw new ParseException("Syntax Error - Valid syntax: paginate [collection] by [items]"); + } + } + + /** + * Renders the tag + * + * @param Context $context + * + * @return string + * + */ + public function render(Context $context) + { + $this->collection = $context->get($this->collectionName); + + if ($this->collection instanceof \Traversable) { + $this->collection = iterator_to_array($this->collection); + } + + if (!is_array($this->collection)) { + // TODO do not throw up if error mode allows, see #83 + throw new RenderException("Missing collection with name '{$this->collectionName}'"); + } + + // How many pages are there? + $this->collectionSize = count($this->collection); + $this->totalPages = ceil($this->collectionSize / $this->numberItems); + + // Whatever there is in the context, we need a number + $this->currentPage = intval($context->get(Liquid::get('PAGINATION_CONTEXT_KEY'))); + + // Page number can only be between 1 and a number of pages + $this->currentPage = max(1, min($this->currentPage, $this->totalPages)); + + // Find the offset and select that part + $this->currentOffset = ($this->currentPage - 1) * $this->numberItems; + $paginatedCollection = array_slice($this->collection, $this->currentOffset, $this->numberItems); + + // We must work in a new scope so we won't pollute a global scope + $context->push(); + + // Sets the collection if it's a key of another collection (ie search.results, collection.products, blog.articles) + $segments = explode('.', $this->collectionName); + if (count($segments) == 2) { + $context->set($segments[0], [$segments[1] => $paginatedCollection]); + } else { + $context->set($this->collectionName, $paginatedCollection); + } + + $paginate = [ + 'page_size' => $this->numberItems, + 'current_page' => $this->currentPage, + 'current_offset' => $this->currentOffset, + 'pages' => $this->totalPages, + 'items' => $this->collectionSize, + ]; + + // Get the name of the request field to use in URLs + $pageRequestKey = Liquid::get('PAGINATION_REQUEST_KEY'); + + if ($this->currentPage > 1) { + $paginate['previous']['title'] = 'Previous'; + $paginate['previous']['url'] = $this->currentUrl($context, [ + $pageRequestKey => $this->currentPage - 1, + ]); + } + + if ($this->currentPage < $this->totalPages) { + $paginate['next']['title'] = 'Next'; + $paginate['next']['url'] = $this->currentUrl($context, [ + $pageRequestKey => $this->currentPage + 1, + ]); + } + + $context->set('paginate', $paginate); + + $result = parent::render($context); + + $context->pop(); + + return $result; + } + + /** + * Returns the current page URL + * + * @param Context $context + * @param array $queryPart + * + * @return string + * + */ + public function currentUrl($context, $queryPart = []) + { + // From here we have $url->path and $url->query + $url = (object) parse_url($context->get('REQUEST_URI') ?: ''); + + // Let's merge the query part + if (isset($url->query)) { + parse_str($url->query, $url->query); + $url->query = array_merge($url->query, $queryPart); + } else { + $url->query = $queryPart; + } + + $url->query = http_build_query($url->query); + + $scheme = $context->get('HTTPS') == 'on' ? 'https' : 'http'; + + return "$scheme://{$context->get('HTTP_HOST')}{$url->path}?{$url->query}"; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagRaw.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagRaw.php new file mode 100644 index 0000000..922fdea --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagRaw.php @@ -0,0 +1,56 @@ +nodelist = []; + + for ($i = 0, $n = count($tokens); $i < $n; $i++) { + if ($tokens[$i] === null) { + continue; + } + $token = $tokens[$i]; + $tokens[$i] = null; + + if ($tagRegexp->match($token)) { + // If we found the proper block delimiter just end parsing here and let the outer block proceed + if ($tagRegexp->matches[1] == $this->blockDelimiter()) { + break; + } + } + + $this->nodelist[] = $token; + } + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagTablerow.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagTablerow.php new file mode 100644 index 0000000..ca9dfc0 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagTablerow.php @@ -0,0 +1,152 @@ +match($markup)) { + $this->variableName = $syntax->matches[1]; + $this->collectionName = $syntax->matches[2]; + + $this->extractAttributes($markup); + } else { + throw new ParseException("Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols:3"); + } + } + + /** + * Renders the current node + * + * @param Context $context + * @throws \Liquid\Exception\RenderException + * @return string + */ + public function render(Context $context) + { + $collection = $context->get($this->collectionName); + + if ($collection instanceof \Traversable) { + $collection = iterator_to_array($collection); + } + + if (!is_array($collection)) { + throw new RenderException("Not an array"); + } + + // discard keys + $collection = array_values($collection); + + if (isset($this->attributes['limit']) || isset($this->attributes['offset'])) { + $limit = $context->get($this->attributes['limit']); + $offset = $context->get($this->attributes['offset']); + $collection = array_slice($collection, $offset, $limit); + } + + $length = count($collection); + + $cols = isset($this->attributes['cols']) ? $context->get($this->attributes['cols']) : PHP_INT_MAX; + + $row = 1; + $col = 0; + + $result = "\n"; + + $context->push(); + + foreach ($collection as $index => $item) { + $context->set($this->variableName, $item); + $context->set('tablerowloop', [ + 'length' => $length, + 'index' => $index + 1, + 'index0' => $index, + 'rindex' => $length - $index, + 'rindex0' => $length - $index - 1, + 'first' => (int)($index == 0), + 'last' => (int)($index == $length - 1), + ]); + + $text = $this->renderAll($this->nodelist, $context); + $break = isset($context->registers['break']); + $continue = isset($context->registers['continue']); + + if ((!$break && !$continue) || strlen(trim($text)) > 0) { + $result .= "$text"; + } + + if ($col == $cols && !($index == $length - 1)) { + $col = 0; + $result .= "\n\n"; + } + + if ($break) { + unset($context->registers['break']); + break; + } + if ($continue) { + unset($context->registers['continue']); + } + } + + $context->pop(); + + $result .= "\n"; + + return $result; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagUnless.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagUnless.php new file mode 100644 index 0000000..7d1f94d --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagUnless.php @@ -0,0 +1,31 @@ +parse(template_source); + * $tpl->render(array('foo'=>1, 'bar'=>2); + */ +class Template +{ + const CLASS_PREFIX = '\Liquid\Cache\\'; + + /** + * @var Document The root of the node tree + */ + private $root; + + /** + * @var FileSystem The file system to use for includes + */ + private $fileSystem; + + /** + * @var array Globally included filters + */ + private $filters = []; + + /** + * @var callable|null Called "sometimes" while rendering. For example to abort the execution of a rendering. + */ + private $tickFunction = null; + + /** + * @var array Custom tags + */ + private static $tags = []; + + /** + * @var Cache + */ + private static $cache; + + /** + * Constructor. + * + * @param string $path + * @param array|Cache $cache + * + * @return Template + */ + public function __construct($path = null, $cache = null) + { + $this->fileSystem = $path !== null + ? new LocalFileSystem($path) + : null; + + $this->setCache($cache); + } + + /** + * @param FileSystem $fileSystem + */ + public function setFileSystem(FileSystem $fileSystem) + { + $this->fileSystem = $fileSystem; + } + + /** + * @param array|Cache|null $cache + * + * @throws \Liquid\Exception\CacheException + */ + public static function setCache($cache) + { + if (is_array($cache)) { + if (isset($cache['cache']) && class_exists($classname = self::CLASS_PREFIX . ucwords($cache['cache']))) { + self::$cache = new $classname($cache); + } else { + throw new CacheException('Invalid cache options!'); + } + } + + if ($cache instanceof Cache) { + self::$cache = $cache; + } + + if (is_null($cache)) { + self::$cache = null; + } + } + + /** + * @return Cache + */ + public static function getCache() + { + return self::$cache; + } + + /** + * @return Document + */ + public function getRoot() + { + return $this->root; + } + + /** + * Register custom Tags + * + * @param string $name + * @param string $class + */ + public static function registerTag($name, $class) + { + self::$tags[$name] = $class; + } + + /** + * @return array + */ + public static function getTags() + { + return self::$tags; + } + + /** + * Register the filter + * + * @param string $filter + */ + public function registerFilter($filter, ?callable $callback = null) + { + // Store callback for later use + if ($callback) { + $this->filters[] = [$filter, $callback]; + } else { + $this->filters[] = $filter; + } + } + + public function setTickFunction(callable $tickFunction) + { + $this->tickFunction = $tickFunction; + } + + /** + * Tokenizes the given source string + * + * @param string $source + * + * @return array + */ + public static function tokenize($source) + { + return empty($source) + ? [] + : preg_split(Liquid::get('TOKENIZATION_REGEXP'), $source, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + } + + /** + * Parses the given source string + * + * @param string $source + * + * @return Template + */ + public function parse($source) + { + if (!self::$cache) { + return $this->parseAlways($source); + } + + $hash = md5($source); + $this->root = self::$cache->read($hash); + + // if no cached version exists, or if it checks for includes + if ($this->root == false || $this->root->hasIncludes() == true) { + $this->parseAlways($source); + self::$cache->write($hash, $this->root); + } + + return $this; + } + + /** + * Parses the given source string regardless of caching + * + * @param string $source + * + * @return Template + */ + private function parseAlways($source) + { + $tokens = Template::tokenize($source); + $this->root = new Document($tokens, $this->fileSystem); + + return $this; + } + + /** + * Parses the given template file + * + * @param string $templatePath + * @throws \Liquid\Exception\MissingFilesystemException + * @return Template + */ + public function parseFile($templatePath) + { + if (!$this->fileSystem) { + throw new MissingFilesystemException("Could not load a template without an initialized file system"); + } + + return $this->parse($this->fileSystem->readTemplateFile($templatePath)); + } + + /** + * Renders the current template + * + * @param array $assigns an array of values for the template + * @param array $filters additional filters for the template + * @param array $registers additional registers for the template + * + * @return string + */ + public function render(array $assigns = [], $filters = null, array $registers = []) + { + $context = new Context($assigns, $registers); + + if ($this->tickFunction) { + $context->setTickFunction($this->tickFunction); + } + + if (!is_null($filters)) { + if (is_array($filters)) { + $this->filters = array_merge($this->filters, $filters); + } else { + $this->filters[] = $filters; + } + } + + foreach ($this->filters as $filter) { + if (is_array($filter)) { + // Unpack a callback saved as second argument + $context->addFilters(...$filter); + } else { + $context->addFilters($filter); + } + } + + return $this->root->render($context); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Variable.php b/package/vendor/liquid/liquid/src/Liquid/Variable.php new file mode 100644 index 0000000..c168fde --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Variable.php @@ -0,0 +1,173 @@ +markup = $markup; + + $filterSep = new Regexp('/' . Liquid::get('FILTER_SEPARATOR') . '\s*(.*)/m'); + $syntaxParser = new Regexp('/(' . Liquid::get('QUOTED_FRAGMENT') . ')(.*)/ms'); + $filterParser = new Regexp('/(?:\s+|' . Liquid::get('QUOTED_FRAGMENT') . '|' . Liquid::get('ARGUMENT_SEPARATOR') . ')+/'); + $filterArgsRegex = new Regexp('/(?:' . Liquid::get('FILTER_ARGUMENT_SEPARATOR') . '|' . Liquid::get('ARGUMENT_SEPARATOR') . ')\s*((?:\w+\s*\:\s*)?' . Liquid::get('QUOTED_FRAGMENT') . ')/'); + + $this->filters = []; + if ($syntaxParser->match($markup)) { + $nameMarkup = $syntaxParser->matches[1]; + $this->name = $nameMarkup; + $filterMarkup = $syntaxParser->matches[2]; + + if ($filterSep->match($filterMarkup)) { + $filterParser->matchAll($filterSep->matches[1]); + + foreach ($filterParser->matches[0] as $filter) { + $filter = trim($filter); + if (preg_match('/\w+/', $filter, $matches)) { + $filterName = $matches[0]; + $filterArgsRegex->matchAll($filter); + $matches = Liquid::arrayFlatten($filterArgsRegex->matches[1]); + $this->filters[] = $this->parseFilterExpressions($filterName, $matches); + } + } + } + } + + if (Liquid::get('ESCAPE_BY_DEFAULT')) { + // if auto_escape is enabled, and + // - there's no raw filter, and + // - no escape filter + // - no other standard html-adding filter + // then + // - add a mandatory escape filter + + $addEscapeFilter = true; + + foreach ($this->filters as $filter) { + // with empty filters set we would just move along + if (in_array($filter[0], ['escape', 'escape_once', 'raw', 'newline_to_br'])) { + // if we have any raw-like filter, stop + $addEscapeFilter = false; + break; + } + } + + if ($addEscapeFilter) { + $this->filters[] = ['escape', []]; + } + } + } + + /** + * @param string $filterName + * @param array $unparsedArgs + * @return array + */ + private static function parseFilterExpressions($filterName, array $unparsedArgs) + { + $filterArgs = []; + $keywordArgs = []; + + $justTagAttributes = new Regexp('/\A' . trim(Liquid::get('TAG_ATTRIBUTES'), '/') . '\z/'); + + foreach ($unparsedArgs as $a) { + if ($justTagAttributes->match($a)) { + $keywordArgs[$justTagAttributes->matches[1]] = $justTagAttributes->matches[2]; + } else { + $filterArgs[] = $a; + } + } + + if (count($keywordArgs)) { + $filterArgs[] = $keywordArgs; + } + + return [$filterName, $filterArgs]; + } + + /** + * Gets the variable name + * + * @return string The name of the variable + */ + public function getName() + { + return $this->name; + } + + /** + * Gets all Filters + * + * @return array + */ + public function getFilters() + { + return $this->filters; + } + + /** + * Renders the variable with the data in the context + * + * @param Context $context + * + * @return mixed|string + */ + public function render(Context $context) + { + $output = $context->get($this->name); + foreach ($this->filters as $filter) { + list($filtername, $filterArgKeys) = $filter; + + $filterArgValues = []; + $keywordArgValues = []; + + foreach ($filterArgKeys as $arg_key) { + if (is_array($arg_key)) { + foreach ($arg_key as $keywordArgName => $keywordArgKey) { + $keywordArgValues[$keywordArgName] = $context->get($keywordArgKey); + } + + $filterArgValues[] = $keywordArgValues; + } else { + $filterArgValues[] = $context->get($arg_key); + } + } + + $output = $context->invoke($filtername, $output, $filterArgValues); + } + return $output; + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..fef1b22 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/src/Hura/App.php b/src/Hura/App.php new file mode 100644 index 0000000..cfbf8b2 --- /dev/null +++ b/src/Hura/App.php @@ -0,0 +1,212 @@ + 'home', + "view" => 'home', + "url" => '/product' + ]; + + protected $data = []; + + + public function __construct() + { + + } + + // start the app + public function start() { + $this->getRouter(); + $this->getData(); + echo $this->renderModule(); + } + + + protected function getRouter() { + /*$route = [ + "module" => (isset($_REQUEST['module'])) ? $_REQUEST['module'] : 'home', + "view" => (isset($_REQUEST['view'])) ? $_REQUEST['view'] : 'home', + ];*/ + + $objRouter = new Router(); + $this->current_route_info = $objRouter->getRouting(); + } + + + protected function getData() { + $module_file = $this->getModuleFile(); + + if(!file_exists($module_file)) { + // print_r($this->current_route_info); + die('Page '. $module_file .' not found!'); + } + + $data = include_once $module_file; + + $global_data = [ + "module" => $this->current_route_info['module'], + "view" => $this->current_route_info['view'], + "url" => $this->current_route_info['url'], + "query" => $this->current_route_info['query'], + ]; + + $this->data = array( + 'global' => $global_data, + // module-specific data, just print {{ page }} to see all available data for the current page!!! + 'page' => (is_array($data)) ? $data : [], + ); + } + + + protected function getModuleFile(): string + { + return SRC_DIR . DIRECTORY_SEPARATOR . join(DIRECTORY_SEPARATOR, [ + "pages", + $this->current_route_info["module"], + str_replace("-", "_", $this->current_route_info["view"]).".php" + ]) ; + } + + + protected function renderModule() { + + if(!$this->current_route_info['module'] || !$this->current_route_info['view']) { + die("Module not exist"); + } + + $template_file_path = $this->tpl_path ."/". $this->current_route_info['module']; + $template_file_name = str_replace("-", '_', $this->current_route_info['view']).".html"; + $template_file_full_path = $template_file_path."/".$template_file_name; + + //check exist + if(!@file_exists( $template_file_full_path)) { + // attempt to auto create first + // todo: this MUST BE TURNED OFF IN PRODUCTION, else many files will be created unintentionally + $module_file = $this->getModuleFile(); + // only create if module file exist + if(file_exists($module_file) && !$this->autoCreateTplFile( $template_file_path, $template_file_name )) { + die("Please manually create template file at: ". $template_file_full_path); + } + } + + $theme_file_path = $this->tpl_path ."/theme.html"; + if( ! @file_exists( $theme_file_path)) { + die("Theme not exist (please create): " . $theme_file_path); + } + + $theme_content = @file_get_contents( $theme_file_path ); + $module_content = @file_get_contents( $template_file_full_path ); + + $page_content_to_parse = preg_replace([ + "/{{(\s+)?page_content(\s+)?}}/" + ], [ + $module_content, + ] , $theme_content ); + + + return $this->parse( + $page_content_to_parse, + $template_file_path + ); + } + + + protected function autoCreateTplFile($file_path, $file_name) : bool { + // create dir if not exist + if(!file_exists($file_path)) { + if(!mkdir($file_path, 0755, true)) { + return false; + } + + if(!file_exists($file_path)) { + return false; + } + } + + //create file + $file_full_path = $file_path . "/". $file_name; + @file_put_contents($file_full_path, $file_full_path); + + return file_exists($file_full_path); + } + + + /* + * 2 ways to render a html template + * 1. Use $html_to_parse, which requires no dependencies + * Example: + * Template::parse(null, 'Age = {{age}}', ['age' => 21], ''); + * + * 2. Use $template_file_path, which requires dependency $path + * Template::parse(Template::$setting_template_path, null, ['age' => 21], 'email/test'); + * */ + protected function parse($html_to_parse = null, $template_file_path = '') { + + if(!$html_to_parse && !$template_file_path) { + return 'Nothing to parse'; + } + + //output to html + Liquid::set('INCLUDE_SUFFIX', 'html'); + Liquid::set('INCLUDE_PREFIX', ''); + //Liquid::set('INCLUDE_ALLOW_EXT', true); + Liquid::set('ESCAPE_BY_DEFAULT', false); + + $enable_cache = false; // default = true, turn this on-off to disable cache while working on local mode + //$enable_cache = true; + + //catch exception and print friendly notice + try { + + $objLiquidTemplate = new LiquidTemplate( $this->tpl_path ); + $objLiquidTemplate->registerFilter( TemplateFilter::class ); + if($enable_cache) { + /*$objLiquidTemplate->setCache(new File([ + 'cache_dir' => self::$cache_dir + ]));*/ + } + + if($html_to_parse) { + $objLiquidTemplate->parse($html_to_parse); + }elseif ($template_file_path) { + $objLiquidTemplate->parseFile($template_file_path); + } + + return $objLiquidTemplate->render($this->data); + + } catch (\Exception $e) { + $result = []; + do { + //printf("%s:%d %s (%d) [%s]\n", $e->getFile(), $e->getLine(), $e->getMessage(), $e->getCode(), get_class($e)); + //echo $e->getTraceAsString(); + //$code = $e->getTrace()[0]['args'][0]; + //if(is_array($code)) $code = serialize($code); + $result[] = sprintf( + " +Lỗi code trong file template html:
+- Chi tiết lỗi: %s
+- File template: %s
+- Hướng dẫn xử lý: Tách từng phần html để kiểm tra và nhấn F5 mỗi lần. Nếu không xuất hiện thông báo này nghĩa là phần đó không tạo lỗi +", + $e->getMessage(), + substr($template_file_path, strrpos($template_file_path, DIRECTORY_SEPARATOR) + 1 ), + //static::$cache_dir + ); + + } while($e = $e->getPrevious()); + + return join(" - ", $result); + } + } + +} diff --git a/src/Hura/Router.php b/src/Hura/Router.php new file mode 100644 index 0000000..48bbc07 --- /dev/null +++ b/src/Hura/Router.php @@ -0,0 +1,78 @@ +path_config = require $path_config_file; + } + + // url: asdas.php?para1=value1 + public function getRouting(): array + { + $parsed = Url::parse($_SERVER['REQUEST_URI']); //abc/product?param1=12¶m2=value2 + //print_r($parsed); + + // home + if($parsed['path'] == '/') { + return [ + 'module' => preg_replace("/[^a-z0-9_\-]/i","", getRequest('module', 'home')), + 'view' => preg_replace("/[^a-z0-9_\-]/i","", getRequest('view', 'home')), + 'view_id'=> 0, + 'query' => $parsed['query'], + 'url' => $_SERVER['REQUEST_URI'], + ]; + } + + // check match pattern in $this->path_config + foreach ($this->path_config as $_config => $_route ) { + if(preg_match("{^".$_config."$}", $parsed['path'], $match )) { + + if(isset($_route['query']) && is_array($_route['query'])) { + $_route['query'] = array_merge($_route['query'], $parsed['query']); + }else{ + $_route['query'] = $parsed['query']; + } + + return array_merge([ + 'path' => $parsed['path'], + 'match' => $match, + ], $_route); + } + } + + // check database + + // else error + return [ + 'module' => "error" , + 'view' => "error" , + 'view_id' => "not_found", + 'query' => $parsed['query'], + 'url' => $_SERVER['REQUEST_URI'], + ]; + + + // auto parse path base on convention: admin/module/view/view_id + /* $ele = explode("/", $parsed['path']); + + $module = $ele[2] ?? 'home'; + $view = isset($ele[3]) ? $ele[3] : getRequest('view', 'home'); + $view_id = isset($ele[4]) ? $ele[4] : getRequest('id', 'view_id'); + + // else error + return [ + 'module' => preg_replace("/[^a-z0-9_\-]/i","", $module ) , + 'view' => preg_replace("/[^a-z0-9_\-]/i","", $view ) , + 'view_id' => preg_replace("/[^a-z0-9_]/i","", $view_id ), + 'query' => $parsed['query'], + 'url' => $_SERVER['REQUEST_URI'], + ];*/ + } + +} diff --git a/src/Hura/TemplateFilter.php b/src/Hura/TemplateFilter.php new file mode 100644 index 0000000..38dcd94 --- /dev/null +++ b/src/Hura/TemplateFilter.php @@ -0,0 +1,256 @@ + '', 'value' => ] from [key1 => value1, key2=>value2, ...] + * + * @param array $key_values [key1 => value1, key2=>value2] + * + * @return array [['key' => 'key1', 'value' => value1], ['key' => 'key2', 'value' => value2]] + */ + public static function to_array(array $key_values) { + $result = []; + foreach ($key_values as $key => $value) { + $result[] = [ + 'key' => $key, + 'value' => $value, + ]; + } + + return $result; + } + + + /** + * split a s by line to create array + * + * @param string $txt + * + * @return array + */ + public static function get_line($txt) { + + if(is_array($txt)) { + return $txt; + } + + $txt = trim($txt); + if( ! $txt ) return []; + + return preg_split("/\n/", $txt); + } + + /** + * Implement strlen + * + * @param string $str + * + * @return int + */ + public static function length($str) { + return strlen(trim($str)); + } + + + /** + * Make number easier to read: 1000 -> 1.000 + * + * @param string $number + * + * @return string + */ + public static function format_number($number) { + if(!$number) return ''; + $number = floatval($number); + + $number = number_format($number, 0, ",", "."); //Vietnamese format with decimals by a coma + + return $number; + } + + public static function format_price($p_price, $currency = ''){ + if(!$p_price) return ''; + if(!$currency) $currency = (defined("DEFAULT_CURRENCY")) ? DEFAULT_CURRENCY : "vnd"; + //if(is_string($p_price)) return 0; + if($currency == 'usd') { + return number_format($p_price,2,".",","); + }else { + return number_format($p_price,0,",","."); + } + } + + /** + * + * Description: get the shop's full asset url for template's images/js/css + * + * //Returns the URL of a file in the "assets" folder of a theme. + // {{ 'shop.css' | asset_url : 'arg1', 'arg2' ...}} -> //cdn.shopify.com/s/files/1/0087/0462/t/394/assets/shop.css?28253 + * + * @param string $file_name + * + * @return string + */ + public static function asset_url($file_name = '') + { + if( !$file_name ) return ''; + + $file_ext = strtolower(strrchr($file_name, ".")); + + // script tags + if(in_array($file_ext, ['.js', '.css'])) return TEMPLATE_ASSET . "/script/" . $file_name; + + // default image + return TEMPLATE_ASSET . "/images/" . $file_name; + } + + + /** + * + * Description: construct a full html tag for images/js/css file + * + * @param string $file_path domain.com/static/style.css?v=3.1.1 + * + * @return string + */ + public static function script_tag($file_path) { + if( ! $file_path ) return ''; + + //check for ? + if(strpos($file_path, "?") !== false) { + $file_ext = str_replace(strrchr($file_path, "?"), "", $file_path); + $file_ext = strtolower(strrchr($file_ext, ".")); + } else { + $file_ext = strtolower(strrchr($file_path, ".")); + } + + $tag_config = [ + ".css" => "", + ".js" => "", + ".jpg" => "\"n\"/", + ".jpeg" => "\"\"/", + ".gif" => "\"\"/", + ".png" => "\"\"/", + ]; + + return (isset($tag_config[$file_ext])) ? $tag_config[$file_ext] : ''; + } + + /** + * {{ product_info.main_image | img_url: '300x300' }} => https://cdn.shopify.com/s/files/1/1183/1048/products/boat-shoes_300x300.jpeg?1459175177 + * @param string $full_path + * @param string $modifier + * $modifier: + * - must be in format: NumberxNumber or Numberx where Number must within 10 -> 9999 + * - or be one of these: small | medium | large + * @return string + */ + public static function img_url($full_path, $modifier) + { + $clean_modifier = ($modifier) ? trim($modifier) : ""; + + // verify $modifier + // must be in format: NumberxNumber or Numberx where Number must within 10 -> 9999 + if($clean_modifier + && !preg_match("/^[0-9]{2,4}x([0-9]{2,4})?$/i", $clean_modifier) + && !in_array($clean_modifier, ["small", "medium", "large"]) + ) { + $clean_modifier = ""; + } + + // return if no valid modifier + if( ! $clean_modifier ) { + return $full_path; + } + + $last_dot_position = strrpos($full_path, "."); + if( ! $last_dot_position ) return $full_path . $clean_modifier; + + return join("", [ + substr($full_path, 0, $last_dot_position), + "_", + $clean_modifier, + substr($full_path, $last_dot_position) + ]); + } + + /** + * //Returns the URL of a file in the Files page of the admin. + //{{ 'size-chart.pdf' | file_url }} -> //cdn.shopify.com/s/files/1/0087/0462/files/size-chart.pdf?28261 + * + * @param string $input + * @param string $string + * + * @return string + */ + public static function file_url($input, $string) + { + return strtoupper($input) . " = " . $string; + } + + /** + * //Returns the asset URL of an image in the Files page of the admin. file_img_url accepts an image size parameter. + //{{ 'logo.png' | file_img_url: '1024x768' }} -> //cdn.shopify.com/s/files/1/0246/0527/files/logo_1024x768.png?42 + * + * @param string $input + * @param string $string + * + * @return string + */ + public static function file_img_url($input, $string) + { + return ''; + } + + + /** + * Show all content of a variable, useful for template development + * + * @param string + * + * @return string + */ + public static function print_r($input) + { + @ob_start(); + print_r($input); + $content = ob_get_contents(); + @ob_end_clean(); + + return join("\r", ['']) ; + } + + /** + * Show all content of a variable, useful for template development + * + * @param string + * + * @return string + */ + public static function show_var($input) + { + @ob_start(); + print_r($input); + $content = ob_get_contents(); + @ob_end_clean(); + + return join("\r", ['']) ; + } +} diff --git a/src/Hura/Url.php b/src/Hura/Url.php new file mode 100644 index 0000000..46e377e --- /dev/null +++ b/src/Hura/Url.php @@ -0,0 +1,47 @@ + '', + 'host' => '', + 'port' => '', + 'user' => '', + 'pass' => '', + 'path' => '', + 'query' => [], + 'fragment' => '', + ]; + foreach ($data as $key => $value) { + if(isset($default[$key])) { + $default[$key] = ($key == 'query') ? self::parsedQuery($value) : $value; + } + } + + return $default; + } + + + public static function parsedQuery($query = '') { + if(!$query) return []; + $result = []; + $parts = explode("&", $query); + + foreach ($parts as $part) { + $el = explode("=", $part); + if(sizeof($el) != 2) continue; + + $cleaned_key = preg_replace("/[^a-z0-9_\-\.]/i", '', $el[0]); + $cleaned_value = preg_replace("/[^a-z0-9_\.\-;&]/i", '', $el[1]); + + $result[$cleaned_key] = $cleaned_value; + } + + return $result; + } +} diff --git a/src/inc/common.php b/src/inc/common.php new file mode 100644 index 0000000..46c1c7f --- /dev/null +++ b/src/inc/common.php @@ -0,0 +1,45 @@ +', $content, '']) ; +} + +function init_autoload(){ + $classLoader = require_once ROOT_DIR . '/package/vendor/autoload.php'; + $classLoader->add("Hura", ROOT_DIR . '/src' ); + + return $classLoader; +} + +//get current paging id +function getPageId(){ + return getRequestInt('page', 1); +} + +function getPageSize($default=10){ + return getRequestInt('pageSize', $default); +} + +//Function to get the post value in submit +function getPost($var, $default="", $encode = false, $keep_tag=""){ + return isset($_POST[$var]) ? $_POST[$var] : $default; +} + +//Function to get the INTERGER request value of a variable +function getRequestInt($var, $min_value = 0){ + $request = isset($_REQUEST[$var]) ? (int) $_REQUEST[$var] : (int) $min_value; + $request = ($request >= $min_value ) ? $request : $min_value; //if user tampers request parameter + return $request; +} + + +//Function to get the request value of a variable +function getRequest($var, $default=""){ + return $_REQUEST[$var] ?? $default; +} + diff --git a/src/inc/fun.db.php b/src/inc/fun.db.php new file mode 100644 index 0000000..c59b662 --- /dev/null +++ b/src/inc/fun.db.php @@ -0,0 +1,16 @@ + + Danh sách quản trị viên + Form thêm/sửa quản trị viên +
+ +
+
+

Cập nhật quản trị

+
+ + + +
+
+
+ + +
+
+ +
+ + Quản lý phòng ban +
+
+
+ + +
+
+
+ + (* tối thiểu 6 ký tự) +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/template/admin/admin_log_product.html b/template/admin/admin_log_product.html new file mode 100644 index 0000000..f5d56a3 --- /dev/null +++ b/template/admin/admin_log_product.html @@ -0,0 +1,45 @@ +
+ + +
+
+

Theo dõi hoạt động tạo sản phẩm

+ +
+ + + + + + + + + + + + + + + + + + + + +
STTQuản trịNgàyTổng Sản phẩm tạoSản phẩm được duyệt
1adminkenli@gmail.com2025-3-12 +
+ 1 - + Xem +
+
0
+ +
+
\ No newline at end of file diff --git a/template/admin/department.html b/template/admin/department.html new file mode 100644 index 0000000..24a321f --- /dev/null +++ b/template/admin/department.html @@ -0,0 +1,48 @@ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
STTPhòng banEmailCodeQuản trị
1Hurasoft test code không được xóaSửa lại
+
+ + + +
\ No newline at end of file diff --git a/template/admin/department_add.html b/template/admin/department_add.html new file mode 100644 index 0000000..9307524 --- /dev/null +++ b/template/admin/department_add.html @@ -0,0 +1,81 @@ +
+ + +
+ + +
+ +
+
+ Thông tin phong ban +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
\ No newline at end of file diff --git a/template/admin/department_entity_permission.html b/template/admin/department_entity_permission.html new file mode 100644 index 0000000..a1905ab --- /dev/null +++ b/template/admin/department_entity_permission.html @@ -0,0 +1,136 @@ +
+ + +
+ + +
+ +
+
+ Cài đặt quyền xem Menu cho: Tiệp Hura +
+

Trong trường hợp quản trị viên không được cập nhật quyền tại đây thì quyền + của quản + trị viên sẽ được + thừa hưởng từ quyền + của phòng ban. +

+

Lưu ý: Quyền xem menu chỉ quy định việc ẩn/hiện nội dung ở Menu, không quy định việc + quản trị viên có thể + xem được nội dung chi tiết của 1 hạng mục nào đó hay không nếu quản trị viên đó biết được + đường link + truy cập. Để hạn + chế quản trị viên xem hạng mục, bạn cần cài đặt ở Quyền hạng mục. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTHạng mụcĐược phép xem
1Bán hàng
1.1Danh sách đơn hàng + +
1.2Chờ mua sản phẩm + +
1.3Thanh toán qua cổng dịch vụ + +
2Sản phẩm
2.1Danh sách Sản phẩm + +
+ +
+
+ +
+ +
+
+
+ + +
\ No newline at end of file diff --git a/template/admin/department_menu_permission.html b/template/admin/department_menu_permission.html new file mode 100644 index 0000000..f5fe073 --- /dev/null +++ b/template/admin/department_menu_permission.html @@ -0,0 +1,135 @@ +
+ + +
+ + +
+ +
+
+ Cài đặt quyền xem Menu cho: Tiệp Hura +
+

Trong trường hợp quản trị viên không được cập nhật quyền tại đây thì quyền + của quản + trị viên sẽ được + thừa hưởng từ quyền + của phòng ban. +

+

Lưu ý: Quyền xem menu chỉ quy định việc ẩn/hiện nội dung ở Menu, không quy định việc + quản trị viên có thể + xem được nội dung chi tiết của 1 hạng mục nào đó hay không nếu quản trị viên đó biết được + đường link + truy cập. Để hạn + chế quản trị viên xem hạng mục, bạn cần cài đặt ở Quyền hạng mục. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTHạng mụcĐược phép xem
1Bán hàng
1.1Danh sách đơn hàng + +
1.2Chờ mua sản phẩm + +
1.3Thanh toán qua cổng dịch vụ + +
2Sản phẩm
2.1Danh sách Sản phẩm + +
+ +
+
+ +
+ +
+
+
+ +
\ No newline at end of file diff --git a/template/admin/entity_permission.html b/template/admin/entity_permission.html new file mode 100644 index 0000000..d4f08f2 --- /dev/null +++ b/template/admin/entity_permission.html @@ -0,0 +1,144 @@ + + +
+
+

Cập nhật quản trị

+
+ + + +
+
+ Cài đặt quyền xem Menu cho: Tiệp Hura +
+ Trong trường hợp quản trị viên không được cập nhật quyền tại đây thì quyền của quản + trị viên sẽ được thừa hưởng từ quyền + của phòng ban. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTHạng mụcĐược phép xem
1Bán hàng
1.1Danh sách đơn hàng +
+ + + +
+
1.2Chờ mua sản phẩm +
+ + + +
+
1.3Thanh toán qua cổng dịch vụ +
+ + + +
+
2Sản phẩm
2.1Danh sách Sản phẩm +
+ + + +
+
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/template/admin/home.html b/template/admin/home.html new file mode 100644 index 0000000..6dcf390 --- /dev/null +++ b/template/admin/home.html @@ -0,0 +1,103 @@ +
+
+

Danh sách quản trị website

+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTHọ tênEmailPhòng banThời gian tạoĐăng nhập lần cuốiIP đăng nhậpXem lịch sửQuản lý
1 +

Khách hàng hunglong.vn

+ Tạm + khóa +
+ hunglong@hurasoft.com + + Khách hàng test giao diện + + 29-01-2024, 1:34 pm + + 07-03-2024, 9:54 am + + 2405:4802:1d21:1660:c4b3:5e6e:3699:f991 + + Xem + toàn + bộ + + +
+
+
\ No newline at end of file diff --git a/template/admin/login_log.html b/template/admin/login_log.html new file mode 100644 index 0000000..8c6b8c5 --- /dev/null +++ b/template/admin/login_log.html @@ -0,0 +1,40 @@ +
+ + +
+
+

Danh sách đăng nhập quản trị

+ +
+ + + + + + + + + + + + + + + + + + +
STTUserIPThời gian
1adminkenli@gmail.com2405:4802:21c:5670:cf0:8f00:98e3:972d + 04-04-2025, 10:17 am +
+ +
+
\ No newline at end of file diff --git a/template/admin/menu_permission.html b/template/admin/menu_permission.html new file mode 100644 index 0000000..b271c97 --- /dev/null +++ b/template/admin/menu_permission.html @@ -0,0 +1,112 @@ + + +
+
+

Cập nhật quản trị

+
+ + + +
+
+ Cài đặt quyền xem Menu cho: Tiệp Hura +
+

Trong trường hợp quản trị viên không được cập nhật quyền tại đây thì quyền của quản + trị viên sẽ được + thừa hưởng từ quyền + của phòng ban. +

+

Lưu ý: Quyền xem menu chỉ quy định việc ẩn/hiện nội dung ở Menu, không quy định việc + quản trị viên có thể + xem được nội dung chi tiết của 1 hạng mục nào đó hay không nếu quản trị viên đó biết được đường link + truy cập. Để hạn + chế quản trị viên xem hạng mục, bạn cần cài đặt ở Quyền hạng mục. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTHạng mụcĐược phép xem
1Bán hàng
1.1Danh sách đơn hàng + +
1.2Chờ mua sản phẩm + +
1.3Thanh toán qua cổng dịch vụ + +
2Sản phẩm
2.1Danh sách Sản phẩm + +
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/template/article/category.html b/template/article/category.html new file mode 100644 index 0000000..ad123d2 --- /dev/null +++ b/template/article/category.html @@ -0,0 +1,163 @@ +
+
+

Danh mục nội dung

+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Danh mục ID Hiển thị STT Số bài viết Lựa chọn
1. Danh mục cấp 1 1 child_article + + + 60 + + Xem + + +
+ + + + + + + + + + +
+
1.1 Danh mục cấp 2 1 child_article + + + 60 + + Xem + + +
+ + + + + + + + + + +
+
1.1.1 Danh mục cấp 3 1 child_article + + + 60 + + Xem + + +
+ + + + + + + + + + +
+
+
+ +
+
\ No newline at end of file diff --git a/template/article/form.html b/template/article/form.html new file mode 100644 index 0000000..4577a85 --- /dev/null +++ b/template/article/form.html @@ -0,0 +1,502 @@ + + +
+
+
+ {% if global.url contains 'id=' %} +

Sửa nội dung

+ +

+ ROG FALCHION LOW PROFILE – CHƠI GAME TRÊN MAC LÀ CHUYỆN NHỎ +

+ {% else %} +

Thêm mới

+ {% endif %} + + +
+ +
+ +
+ +
+
+

Thông tin cơ bản

+ +
+

Ảnh đại diện

+ +
+ +
+ + +
+ +
+

Ảnh nền trong chi tiết

+ +
+ +
+ + +
+ +
+

Danh mục * có thể chọn nhiều danh + mục 

+ +
+
+ +
+ +
+ +

Danh mục cấp 1

+ +
+ +
+ + +
+

Danh mục cấp 2

+ +
+
+
+
+ + +
+
+

Danh mục cấp 2

+

+
+
+
+
+
+
+
+ + +
+
+

Danh mục cấp 1

+
+
+
+
+
+
+
+
+
+ +
+
+ +
+

Tiêu đề

+ + +
+ +
+

Link Index

+ + +
+ +
+

Link tham khảo (link web ngoài nếu có)

+ + +
+ +
+

Tóm tắt chính

+ + +
+ +
+

Tags (Mỗi cụm từ 1 dòng)

+ + +
+
+ + +
+ + +
+ +
+ +
+

Thứ tự (* số cao xếp + trước)

+ + +
+
+ + +
+

Dùng cho SEO

+ +
+

+ Url canonical + * để trống sẽ dùng link mặc định của hệ thống +

+ + +
+ +
+

+ Meta Title +  0 ký tự , 0 từ +

+ + +
+ +
+

+ Meta Keywords +  0 ký tự , 0 từ +

+ + +
+ +
+

+ Meta Description + 0 ký tự , 0 từ (Khuyến nghị: 160 ký tự) +

+ + +
+ +
+

Cho phép Google Index

+ + +
+ +
+

Hiển thị Mục lục đầu bài viết

+ + +
+ +
+ + +
+

Quản trị hiển thị

+ + +
+

Thời gian của bài viết

+ +
+ + + + + +
+ + (Hiển thị cho người xem thời gian thật của + bài thay vì thời gian cập nhật) +
+ + +
+

Trạng thái

+ +
+ + + + + + +
+
+ + + + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/template/article/home.html b/template/article/home.html new file mode 100644 index 0000000..e6e433f --- /dev/null +++ b/template/article/home.html @@ -0,0 +1,137 @@ +
+
+

Bài viết

+ +
+ + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Sản phẩm (Tổng số: 1.742) Thống kê Quản trị Lựa chọn
1 + + + [#2481] + Elgato – Happy New Year 2024 Deal Hot Đầu Xuân + +
Thời gian chương trình : 15.02.2024 – 24.02.2024
+ +
+ STT + +
+
+

- Xem: 90

+

- Thích: 0

+

- Comment: 0

+
+

Cập nhật: Nguyễn MInh Hải

+

(15-02-2024, 2:57 pm)

+
+
+ + + + + + + + + + + + + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+ +
+
\ No newline at end of file diff --git a/template/article/relation.html b/template/article/relation.html new file mode 100644 index 0000000..526465b --- /dev/null +++ b/template/article/relation.html @@ -0,0 +1,124 @@ +{% assign _check_type = global.url | split: 'related_item_type=' %} + + + +
+
+ +
+

Sửa nội dung

+ +

ROG FALCHION LOW PROFILE – CHƠI GAME TRÊN MAC LÀ + CHUYỆN NHỎ

+ + +
+ + + + + +
+ {% if _check_type[1] == 'product'%} + + {% include 'article/relation_product' %} + + {% elsif _check_type[1] == 'product-category' %} + + {% include 'article/relation_product_category' %} + + {% endif %} +
+
+
+ + + + \ No newline at end of file diff --git a/template/article/relation_add.html b/template/article/relation_add.html new file mode 100644 index 0000000..053164c --- /dev/null +++ b/template/article/relation_add.html @@ -0,0 +1,70 @@ +{% assign _check_type = global.url | split: 'related_item_type=' %} + + + +
+
+
+

Thêm liên quan

+ +

+ Thêm liên quan: HuraSoft - Test tin tức +

+
+ + + + {% if _check_type[1] == 'product' %} + + {% include 'article/relation_product_add' %} + + {% elsif _check_type[1] == 'product-category' %} + + {% include 'article/relation_product_category_add' %} + + {% endif %} + +
+
+ + + + + + + \ No newline at end of file diff --git a/template/article/relation_product.html b/template/article/relation_product.html new file mode 100644 index 0000000..8965d02 --- /dev/null +++ b/template/article/relation_product.html @@ -0,0 +1,68 @@ +
+

Danh sách liên quan: HuraSoft - Test tin tức

+ + + + + Thêm liên quan mới + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Tên Thông tin bán hàng Cài đặt Lựa chọn
1 + CPU Intel Pentium Gold G6405 (Intel LGA1200 - 2 Core - 4 Thread - Base 4.1Ghz - Cache 4MB + +

- Mã kho :  CPUI0132

+

- Lượt xem:  21577

+

- Giá bán :  1.999.000 vnđ

+

- Kho hàng : 3

+

- Bảo hành : 36 Tháng

+
+

STT

+ +
+ +
+
+ +
+ + 1 + 2 + 3 + 4 + +
\ No newline at end of file diff --git a/template/article/relation_product_add.html b/template/article/relation_product_add.html new file mode 100644 index 0000000..02b281c --- /dev/null +++ b/template/article/relation_product_add.html @@ -0,0 +1,78 @@ +
+

Chọn sản phẩm liên quan - Tổng số: 28872

+ +
+ + +

Chọn danh mục

+ + + + + + +
+ + + Chọn toàn bộ danh sách + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Tên Thông tin bán hàng Lựa chọn
1 + + + CPU Intel Pentium Gold G6405 Intel LGA1200 - 2 Core - 4 Thread - Base 4.1Ghz - Cache 4MB +

Cập nhật: Hôm nay, 11:11 am

+
+ - Mã kho : CPUI0132
+ - Lượt xem: 21577
+ - Giá bán : 1.999.000 vnđ
+ - Kho hàng : 3
+ - Bảo hành : 36 Tháng +
+ + Chọn liên quan + +
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
\ No newline at end of file diff --git a/template/article/relation_product_category.html b/template/article/relation_product_category.html new file mode 100644 index 0000000..9ab92fd --- /dev/null +++ b/template/article/relation_product_category.html @@ -0,0 +1,53 @@ +
+

Danh sách liên quan: HuraSoft - Test tin tức

+ + + + + Thêm liên quan mới + +
+ +
+ + + + + + + + + + + + + + + + + + + +
STT Tên Lựa chọn
1 +

Laptop - Tablet - Mobile

+ +

STT

+ +
+ + Bỏ lựa chọn + +
+
+ +
+ + 1 + 2 + 3 + 4 + +
\ No newline at end of file diff --git a/template/article/relation_product_category_add.html b/template/article/relation_product_category_add.html new file mode 100644 index 0000000..2102cf2 --- /dev/null +++ b/template/article/relation_product_category_add.html @@ -0,0 +1,60 @@ +
+

Chọn danh mục liên quan

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Danh mục Lựa chọn
1 Danh mục cấp 1 + + Chọn liên quan + + +

Đã chọn

+
1.1 Danh mục cấp 2 + + Chọn liên quan + +
1.1.1 Danh mục cấp 3 + + Chọn liên quan + +
1.1.1.1 Danh mục cấp 4 + + Chọn liên quan + +
+
+
\ No newline at end of file diff --git a/template/article/tag.html b/template/article/tag.html new file mode 100644 index 0000000..2e5da21 --- /dev/null +++ b/template/article/tag.html @@ -0,0 +1,123 @@ + + +
+
+ +
+

Sửa nội dung

+ +

ROG FALCHION LOW PROFILE – CHƠI GAME TRÊN MAC LÀ CHUYỆN NHỎ

+ + +
+ + +
+ Chỉnh sửa + + {% if global.url contains 'id=' %} + Tag + Liên quan: Sản phẩm + Liên quan: Danh mục sản phẩm + {% endif %} +
+ + +
+ + + Chọn tag mới + + +
+ + + + + + + + + + + + + + + + + +
STT Tag Lựa chọn
1 Laptop GTX 1050  + +
+
+ +
+ + +
+
+
+ + + + \ No newline at end of file diff --git a/template/banner/home.html b/template/banner/home.html new file mode 100644 index 0000000..b97f70f --- /dev/null +++ b/template/banner/home.html @@ -0,0 +1,200 @@ +
+
+

Danh sách banner

+ +
+ + + + + + +
+ +
+
+ + + + + Chọn + danh mục sản phẩm để xem +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Thông tin Thứ tự Click Chỉnh sửa
1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tên gọi:
Link tới:
File:/media/banner/21_06-1f22593caab42ff1e4ebf38094a75afe.jpeg
Kích thước:Rộng x Cao (Width x Height) = 3130px x 2075px
Link:/ad.php?id=236
Thời gian hiển thị:Luôn hiển thị, chỉ ẩn khi hạ bằng tay
Cập nhật:21-06-2023, 11:08 am
+
0 0 +
+ + + + + + + + +
+
2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tên gọi:
Link tới:
File:/media/banner/21_06-1f22593caab42ff1e4ebf38094a75afe.jpeg
Kích thước:Rộng x Cao (Width x Height)=3130px x 2075px
Link:/ad.php?id=236
Thời gian hiển thị:Luôn hiển thị, chỉ ẩn khi hạ bằng tay
Cập nhật:21-06-2023, 11:08 am
+
0 0 +
+ + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+ +
+
\ No newline at end of file diff --git a/template/banner/location.html b/template/banner/location.html new file mode 100644 index 0000000..9c79c34 --- /dev/null +++ b/template/banner/location.html @@ -0,0 +1,67 @@ + + +
+

Quản lý vị trí hiển thị banner

+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STT ID Template File Vị trí Ngày tạo Chỉnh sửa
1 +

ID: 43

+

banner_webmau4_mb

+
homepage Banner MB Webmau4 30-11-2024, 12:00 am +
+ Xem + banner + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
\ No newline at end of file diff --git a/template/banner/location_add.html b/template/banner/location_add.html new file mode 100644 index 0000000..5183df6 --- /dev/null +++ b/template/banner/location_add.html @@ -0,0 +1,123 @@ + + +
+
+
+

Cập nhật lại vị trí banner

+ + + + + Thêm vị trí mới + + +

+ banner_under_slider_mobile_webmau3 +

+
+ + + +
+

Thông tin cơ bản

+ + +
+
+ Chọn template hiển thị + +
+ + + +
+
+ + + +
+ + +
+
+ Mã vị trí (*) * chỉ gồm ký tự a-z0-9 và dấu _ + +
+ + + +
+
+ + +
+ + +
+
+ Đặt tên vị trí (*) + +
+ + + +
+
+ + +
+ + +
+

Mô tả tóm tắt

+ + +
+ +
+ + +
+
+
\ No newline at end of file diff --git a/template/banner/upload.html b/template/banner/upload.html new file mode 100644 index 0000000..8044da8 --- /dev/null +++ b/template/banner/upload.html @@ -0,0 +1,417 @@ + + +
+ + +
+ {% if global.url contains 'id=' %} + {% endif %} +

+ Cập nhật lại banner

+
+ + +
+ + + + + + + + + + + +
+

File

+
+

+ /media/banner/21_06-1f22593caab42ff1e4ebf38094a75afe.jpeg

+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Link theo dõi /ad.php?id=236
Đặt tên banner để theo dõi + +
Mô tả + +
Kích thước file + Chiều dài (width) : + px - + Chiều cao (height) + px + (* bắt buộc cho file flash) +
File banner +
+ Upload từ máy tính + +
+ +
+

Hoặc điền địa chỉ đầy đủ trên mạng (v.d. + http://web-acb.com/anh.jpg):

+ + +
+
Địa chỉ URL đích + +
Thứ tự hiển thị + + (nếu nhiều banner cùng 1 vị trí, số cao xếp trước) +
Thời gian hiển thị + + + + +
+

Bắt đầu

+
+ + +
+ +

Giờ

+ + +
+ +
+

Kết thúc

+
+ + +
+ +

Giờ

+ + +
+
Hiển thị + + + +
Khác + +
+
+ + +
+
+ +
+
+ + + \ No newline at end of file diff --git a/template/customer/comment.html b/template/customer/comment.html new file mode 100644 index 0000000..c57aa9e --- /dev/null +++ b/template/customer/comment.html @@ -0,0 +1,211 @@ +
+
+

Tổng hợp trao đổi

+ + + +
+ + +
+ + +
+

Tổng số: 1.996

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
STTPhản hồiThời gianTác giảSản phẩmNội dungLựa chọn
1109-08-2023, 11:24 am +
+ Nguyễn An +

Quản + trị + viên

+
+

annv@hurasoft.com

+
+ Màn Hình AOC 24B1XHS 23.8 inch Full HD + + test reply + + + +
+
+ + + + + + + +
+
\ No newline at end of file diff --git a/template/customer/customer_add.html b/template/customer/customer_add.html new file mode 100644 index 0000000..f7c0fdb --- /dev/null +++ b/template/customer/customer_add.html @@ -0,0 +1,170 @@ +
+ + +
+
+
+
+

Cập nhật lại khách hàng

+ Khách hàng: Nguyễn Duy Thành +
+ + + + Thêm khách hàng mới + +
+
+ +
+
+ +
+
+
+ Thông tin khách hàng +
+ Thuộc tính khách hàng + Quản lý nhóm khách hàng +
+ +
+ + + +
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+ +
+ + + + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/template/customer/customer_contact.html b/template/customer/customer_contact.html new file mode 100644 index 0000000..4365e9d --- /dev/null +++ b/template/customer/customer_contact.html @@ -0,0 +1,73 @@ +
+
+

Khách hàng liên hệ

+ +
+ + +
+ + +
+

Tổng hợp các liên hệ của khách hàng từ form liên hệ trên trang liên + hệ của website

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STTThời gianKhách hàngThông tinNội dung liên hệGhi chúTrạng thái
109-08-2023, 11:24 amĐức Anh +

- Email: ducanhnguyen199x@gmail.com

+

- Tel: 0976642092

+

- Mobile: 0976642092

+
+ Trả góp sản phẩm: HHWS AMD RYZEN 9 3900XT | 16G | NVIDIA RTX + 2060 6G +

- Tên: no name

+

- SĐT: 0147258369

+

- Nội dung: fsfdf

+
+ + Cập + nhật + + +
+ + +
+
+
+
+
\ No newline at end of file diff --git a/template/customer/customer_group.html b/template/customer/customer_group.html new file mode 100644 index 0000000..b9f0f94 --- /dev/null +++ b/template/customer/customer_group.html @@ -0,0 +1,53 @@ +
+
+

Nhóm khách hàng

+ + + + + + + + + + + + + + + + + + + + + + +
STTMã nhómTên gọi/miêu tảKhách hàngChỉnh sửa
10Khách vip + Xem + danh sách + + +
+ +
+
\ No newline at end of file diff --git a/template/customer/customer_group_add.html b/template/customer/customer_group_add.html new file mode 100644 index 0000000..edd7c34 --- /dev/null +++ b/template/customer/customer_group_add.html @@ -0,0 +1,71 @@ +
+ + + +
+
+
+
+

Cập nhật nhóm khách hàng

+
+ + + Thêm nhóm khách hàng + +
+

Hướng dẫn: Nhóm khách hàng bao gồm các khách hàng cùng chia sẻ một số đặc + điểm + nhất định, giúp cho việc + quản lý và chăm + sóc khách hàng tốt hơn. Ví dụ các nhóm khách hàng thường được tạo là: Khách hàng thân thiết, Khách hàng + nữ, Khách hàng + VIP, Khách văn phòng v..v. +

+
+ +
+
+ +
+
+
+
+ + +
+
+ + (* Dùng phân biệt từng nhóm, Mã chỉ bao gồm các ký tự + a-z0-9 và _, + không dấu và không có + dấu cách. Ví dụ: vip1, dai_ly + ...) + +
+
+ + +
+ +
+
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/template/customer/customer_group_info.html b/template/customer/customer_group_info.html new file mode 100644 index 0000000..2bffc6c --- /dev/null +++ b/template/customer/customer_group_info.html @@ -0,0 +1,74 @@ +
+ + + +
+
+

Thông tin khách hàng liên hệ

+

Lê Văn Ninh

+
+ +
+
+ +
+
+
+
+ Thông tin liên hệ +
+ Họ tên: +

Lê Văn Ninh

+
+
+ Địa chỉ email: +

no_email_1708170779@mail.com

+
+
+ Số điện thoại: +

0356723619

+
+
+ Thời gian liên hệ: +

17-02-2024, 6:56 pm

+
+
+ +
+ Nội dung chi tiết +

+ Tìm sản phẩm cần định giá: + - Mẫu máy cần thu: intel i3-4160 CPU @ 3.600Hz 3.60 Ghz + - Địa chỉ: Phường Quảng Cư ,Tp.Sầm Sơn + - Mô tả ngắn về ngoại hình và tình trạng thiết bị: đang dùng tốt + - Sản phẩm bạn cần tư vấn lên đời: CPU INTEL CORE + I3-12100F (3.3GHZ TURBO UP TO 4.3GHZ, 4 NHÂN 8 LUỒNG, 12MB CACHE, 58W)- SOCKET INTEL LGA + 1700) +

+
+ +
+ + +
+ +
+
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/template/customer/customer_newsletter.html b/template/customer/customer_newsletter.html new file mode 100644 index 0000000..b4f4910 --- /dev/null +++ b/template/customer/customer_newsletter.html @@ -0,0 +1,58 @@ +
+
+

Khách hàng góp ý

+ + + +
+ + + + + + + + + + + + + + + + + + + + + +
STTHọ tênEmailNgày đăng kýThông tin khácLựa chọn
1Trung Hiếutrunghieu27072009@gmail.com19-07-2023, 4:45 pmtest reply + +
+
+
+
\ No newline at end of file diff --git a/template/customer/customer_review.html b/template/customer/customer_review.html new file mode 100644 index 0000000..e42b354 --- /dev/null +++ b/template/customer/customer_review.html @@ -0,0 +1,80 @@ +
+
+

Khách hàng góp ý

+
+

Tổng hợp các liên hệ của khách hàng từ form liên hệ trên trang liên hệ của website

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTKhách hàngNội dungLựa chọn
3 +

huyth.c2hoangdieu.haugiang@gmail.com

+

Trịnh Hoàng Huy

+

29-01-2018, 1:34 am

+
+ Góp ý (rate:0) +

cửa hàng nên có thêm các dòng SSD Samsung 960 PRO và Samsung 960 EVO để có thêm nhiều lựa + chọn

+
+ + + +
3 +

huyth.c2hoangdieu.haugiang@gmail.com

+

Trịnh Hoàng Huy

+

29-01-2018, 1:34 am

+
+ Góp ý (rate:0) +

cửa hàng nên có thêm các dòng SSD Samsung 960 PRO và Samsung 960 EVO để có thêm nhiều lựa + chọn

+
+ + + +
3 +

huyth.c2hoangdieu.haugiang@gmail.com

+

Trịnh Hoàng Huy

+

29-01-2018, 1:34 am

+
+ Góp ý (rate:0) +

cửa hàng nên có thêm các dòng SSD Samsung 960 PRO và Samsung 960 EVO để có thêm nhiều lựa + chọn

+
+ + + +
+
+
+
\ No newline at end of file diff --git a/template/customer/home.html b/template/customer/home.html new file mode 100644 index 0000000..6b88841 --- /dev/null +++ b/template/customer/home.html @@ -0,0 +1,128 @@ +
+
+

Danh sách khách hàng

+ + + +
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTIDKhách hàngThông tinTỉnh/TPTrạng tháiSố đơn hàngTổng giá trịThời gianChỉnh sửa
11767Đức Anh +

- Email: ducanhnguyen199x@gmail.com

+

- Tel: 0976642092

+

- Mobile: 0976642092

+
+ Hà Nội + +
Hoạt động
+
+ 0Thành công + + 0Thu + +

+ - Tham gia: 06-10-2023, 10:46 am +

+

+ - Đăng nhập cuối: Chưa bao giờ +

+
+ + +
+
+ +
+ + + + 1 + 2 + 3 + 4 + 5 + + + +
+ +
+
\ No newline at end of file diff --git a/template/customer/review.html b/template/customer/review.html new file mode 100644 index 0000000..a65d9cf --- /dev/null +++ b/template/customer/review.html @@ -0,0 +1,211 @@ +
+
+

Tổng hợp đánh giá

+ + + +
+ + +
+ + +
+

Tổng số: 1.996

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
STTPhản hồiThời gianTác giảSản phẩmNội dungLựa chọn
1109-08-2023, 11:24 am +
+ Nguyễn An +

Quản + trị + viên

+
+

annv@hurasoft.com

+
+ Màn Hình AOC 24B1XHS 23.8 inch Full HD + + test reply + + + +
+
+ + + + + + + +
+
\ No newline at end of file diff --git a/template/deal/deal_add.html b/template/deal/deal_add.html new file mode 100644 index 0000000..1d40c56 --- /dev/null +++ b/template/deal/deal_add.html @@ -0,0 +1,384 @@ +{% if global.url contains '?id=' %} + +{% include 'deal/deal_detail' %} + +{% else %} + + + +
+

Chọn sản phẩm Deal

+ +
+ + +
+ +
+ + +
+ + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
STT Sản phẩm (Tổng số: 1.742) Thông tin khác Lựa chọn
1 + Màn hình Viewsonic VA3209U-4K (32 inch/UHD/IPS/60Hz/4ms) +

Mã kho: MOVI213

+

Danh mục: Màn Hình Máy Tính, Màn Hình Theo Hãng, Màn Hình ViewSonic

+

Bảo hành: 36 Tháng

+

Khuyến mại: + "ƯU ĐÃI Giảm ngay 10% vào thẻ nhớ khi khách hàng mua kèm Camera"

+
+

Giá: 10.599.000

+

Kho hàng: 0

+

Bảo hành: 36 Tháng

+
+ + Chọn sản phẩm + +
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
+ + + + + + + + + + + + + + +{% endif %} \ No newline at end of file diff --git a/template/deal/deal_detail.html b/template/deal/deal_detail.html new file mode 100644 index 0000000..5474c97 --- /dev/null +++ b/template/deal/deal_detail.html @@ -0,0 +1,294 @@ + + +
+
+ +
+

Cập nhật deal

+ + + +

+ Màn hình máy tính HKC M20A6 20 Inch Full HD +

+
+ + +
+ +
+ + +
+ +

Thông tin deal

+ +
+

Tiêu đề của Deal

+ + +
+ +
+

Sản phẩm liên quan

+ +

+ Màn hình máy tính HKC M20A6 20 Inch Full HD + Xem link +

+
+ +
+

Mô tả tóm tắt về deal nếu có

+ + +
+ +

+ +

Thông tin bán Sản phẩm

+ +
+

Giá sản phẩm

+ +

17.999.000 VNĐ

+
+ +
+

Giá deal

+ + +
+ +
+
+

+ Số lượng bán + (Số đang có: 8) +

+ + +
+ +
+
+ Số lượng tối thiểu phải mua + +
+ + + +
+
+ + +
+ +
+
+ Số lượng tối đa được mua + +
+ + + +
+
+ + +
+
+ +
+

Thời gian hiệu lực *

+ +
+

Bắt đầu

+
+ + +
+ +

Giờ

+ + +
+ +
+

Kết thúc

+
+ + +
+ +

Giờ

+ + +
+
+ +
+

Thứ tự hiển thị

+ + +
+ + +
+

Hiển thị

+ + + + +
+ +

+ + +
+
+
\ No newline at end of file diff --git a/template/deal/home.html b/template/deal/home.html new file mode 100644 index 0000000..1efed49 --- /dev/null +++ b/template/deal/home.html @@ -0,0 +1,178 @@ +
+
+

Danh sách Deal

+ +
+ + + + + + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + STT Ảnh Sản phẩm (Tổng số: 1.742) Thông tin Deal Thời gian Deal + + Công cụ
+ + 1 +
+ +
+
+ HH WORKSTATION CORE i9 10900K | 32GB | NVIDIA RTX 2060 6GB +

[SKU: LTLE950]

+

Số lượng: 60

+

Số tối thiểu cho 1 đơn hàng: 1

+

Tạo lúc: 25-10-2022, 4:56 pm

+

Cập nhật: 17-01-2024, 9:36 am

+
+

Giá Deal: + 35.000.000 vnđ + (Giảm: 4% ) +

+

Giá thị trường: 36.000.000 vnd

+

Số đơn hàng : 6

+

Số lượng đặt mua: 10

+

Lượt xem: 0

+
+ 01-01-2024, 1:00 pm đến + 31-03-2024, 8:00 pm + + + + + + + +
+ + + + + + + + + + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
+
+ + + + \ No newline at end of file diff --git a/template/deal/upload_deal_excel.html b/template/deal/upload_deal_excel.html new file mode 100644 index 0000000..42fa9ee --- /dev/null +++ b/template/deal/upload_deal_excel.html @@ -0,0 +1,127 @@ + + +
+
+ +
+

Thêm deal qua excel

+ +

Hướng dẫn: File sẽ thêm Sản phẩm nếu chưa có, và cập nhật Sản phẩm nếu có rồi

+ + + + Tải danh sách deal + +
+ + + + + +
+ +
+ Chú ý: Yêu cầu file excel theo đúng file mẫu. + + + + Tải file mẫu + +
+ + + + + + + + + + + + + +
File cập nhật + +
Cấu hình cột Excel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Cột ExcelNội dung
AMã kho (SKU)
BTên của deal
CGiá deal (dạng số)
DSố lượng mua tối thiểu (dạng số - mặc định là 1)
ESố lượng mua tối đa (dạng số - mặc định là 1)
FSố lượng (dạng số)
GNgày bắt đầu (DD/MM/YYYY)
HGiờ bắt đầu (hh:mm)
INgày kết thúc (DD/MM/YYYY)
JGiờ kết thúc (hh:mm)
KChọn hiển thị (yes|no)
LThứ tự hiển thị
+ +
+ +
+ + +
+
+
\ No newline at end of file diff --git a/template/distributor/form.html b/template/distributor/form.html new file mode 100644 index 0000000..b369377 --- /dev/null +++ b/template/distributor/form.html @@ -0,0 +1,170 @@ +
+ + +
+
+
+

Form đại lý

+ +
+

Nhà Thuốc Hiệp Phát

+
+
+
+ +
+
+
+ Cập nhật lại + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tên gọi + +
Mô tả + +
Tọa độ bản đồ +
+
+

Kinh độ

+ +
+ +
+

Vĩ độ

+ +
+
+
Tỉnh/TP + +
Quận huyện + +
Địa chỉ + +
Điện thoại + +
Website + +
Người liên hệ + +
Email liên hệ + +
STT + +
STT + +
Trạng thái +
+ + + +
+
+ +
+ +
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/template/distributor/home.html b/template/distributor/home.html new file mode 100644 index 0000000..969ed61 --- /dev/null +++ b/template/distributor/home.html @@ -0,0 +1,106 @@ +
+
+

Danh sách đại lý

+ +
+ + +
+ +
+
+

Lựa chọn ngôn ngữ

+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTĐại lýNGười liên hệĐịa chỉTỉnhĐiện thoạiLựa chọn
1Nhà Thuốc Hiệp PhátNguyễn Thị HiệpThửa đất số 126, Tờ BĐS 53, KP Phú Lạc, P.Hòa Hiệp Nam, TX Đông Hòa, PYPhú yên0982483346 + +
1Nhà Thuốc Hiệp PhátNguyễn Thị HiệpThửa đất số 126, Tờ BĐS 53, KP Phú Lạc, P.Hòa Hiệp Nam, TX Đông Hòa, PYPhú yên0982483346 + +
+ + +
+
\ No newline at end of file diff --git a/template/home/components/main_stat.html b/template/home/components/main_stat.html new file mode 100644 index 0000000..ad71f35 --- /dev/null +++ b/template/home/components/main_stat.html @@ -0,0 +1,210 @@ +
+
+
+

+ Đơn + hàng + mới

+ + + + +
+ +

10.000.000

+
+ +
+
+ + 10% +
+ +

Hôm qua

+
+
+ +
+
+
+

Khách + hàng + liên hệ qua website

+ + + + +
+ +

10.000.000

+
+ +
+
+ + 10% +
+ +

Hôm qua

+
+
+ +
+
+
+

Đơn + trả + góp

+ + + + +
+ +

10.000.000

+
+ +
+
+ + 10% +
+ +

Hôm qua

+
+
+ +
+
+
+

Đơn + hoàn + trả

+ + + + +
+ +

10.000.000

+
+ +
+
+ + 10% +
+ +

Hôm qua

+
+
+ +
+
+
+
32.4k
+

Doanh thu trong tuần này

+
+ +
+ 12% + +
+
+ +
+ +
+
+ + + + + Users Report + + + +
+
+
+ +
+
+
3.4k
+ +
+

Truy cập web trong tuần này

+ + + + 42.5% + +
+
+ +
+
+
Lượt truy cập: +
+
1.458
+
+ +
+
Người xem:
+
2.130
+
+
+ +
+ +
+
+ + + + Leads Report + + +
+
+
\ No newline at end of file diff --git a/template/home/home.html b/template/home/home.html new file mode 100644 index 0000000..f957780 --- /dev/null +++ b/template/home/home.html @@ -0,0 +1,850 @@ + + +
+
+ + +
+
+
+ + Việc cần làm +
+ +
+
+
+
+
+
+ +
+ Xem +
+
+

Đơn hàng mới

+ + 52.4% + +
+ +
+ 6 + + 10 + +
+
+
+
+
+
+
+ +
+ Xem +
+
+

Liên hệ mới

+ + 52.5% + +
+ +
+ 6 + + 10 + +
+
+
+
+
+
+
+ +
+ Xem +
+
+

Lỗi web

+ + 10% + +
+ +
+ 1 + + 5 + +
+
+
+
+
+
+
+ +
+ Xem +
+
+

Đơn trả góp mới

+ + 90% + +
+ +
+ 9 + + 10 + +
+
+
+
+ + + + + + +
+ + +
+
+
+ + Tóm tắt +
+
+
+ +
+
+
+
+

+ Đơn hàng mới +

+
+

+ 100 +

+
+
+
+ +
+
+
+
+ + 10.8% +
+

+ Hôm qua +

+
+
+
+ +
+
+
+
+

+ Khách hàng liên hệ + qua website +

+
+

+ 12 +

+
+
+
+ +
+
+
+
+ + 5.4% +
+

+ Hôm qua +

+
+
+
+ +
+
+
+
+

+ Bình luận mới dược phê duyệt +

+
+

+ 20 +

+
+
+
+ +
+
+
+
+ + -6.8% +
+

+ Hôm qua +

+
+
+
+ +
+
+
+
+

+ Lỗi website +

+
+

+ 30 +

+
+
+
+ +
+
+
+
+ + 8.5% +
+

+ Hôm qua +

+
+
+
+
+ +
+
+ + +
+
+
+ + Doanh thu + + +
+
+
+ + 184.78K + + + +3.24% + +
+ + Tổng doanh thu trong năm nay + +
+
+
+
+ + +
+
+ + +
+
+
+ + Truy cập khách hàng + +
+
+ + Dự đoán + +
+
+
+
+
+

Advertise

+

+ $148 +

+
+ +

4.78%

+
+
+ +
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+ +

Hoạt động gần đây

+
+
    + +
  • + +
    +

    Đơn hàng #12345 đã được xử lý

    + Vừa xong +
    +
  • + +
  • + 💬 +
    +

    Khách hàng Nguyễn Văn A đã gửi phản hồi

    + 3 phút trước +
    +
  • + +
  • + ⚠️ +
    +

    Lỗi kết nối với cổng thanh toán

    + 4 phút trước +
    +
  • + +
  • + 📰 +
    +

    Tin tức mới: Xu hướng mua sắm trực tuyến 2024

    + 5 phút trước +
    +
  • +
  • + +
    +

    Đơn hàng #12345 đã bị hủy

    + Vừa xong +
    +
  • + +
  • + 💬 +
    +

    Khách hàng Nguyễn Văn A đã gửi phản hồi

    + 3 phút trước +
    +
  • + +
  • + ⚠️ +
    +

    Lỗi kết nối với cổng thanh toán

    + 4 phút trước +
    +
  • + +
  • + 📰 +
    +

    Tin tức mới: Xu hướng mua sắm trực tuyến 2024

    + 5 phút trước +
    +
  • +
+
+
+
+ + +
+
+ + +
+
+
+ + Ghi nhắc việc làm cá nhân +
+ +
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Nội dung Hết hạn Trạng thái Sửa
1 Sửa trang chủ websiteLaptoptcc 28/11/2023 +
Đang triển khai +
+
+ +
2 Sửa trang chủ websiteLaptoptcc 28/11/2023 +
Đang triển khai +
+
+ +
3 Sửa trang chủ websiteLaptoptcc 28/11/2023 +
Đang triển khai
+
+ +
4 Sửa trang chủ websiteLaptoptcc 28/11/2023 +
Đang triển khai +
+
+ +
+
+
+ + +
+
+ + Ghi chú nhanh +
+ + + + +
+ + + + + + + + + + + + + + + +
STTGhi chúCông cụ
1Nội dung ghi chú + +
+
+ +
+ + + +
+
+
+ + Hỗ trợ từ Hurasoft +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTYêu cầuPhản hồi cuốiTrạng thái
1 + + +
+

[Hura]

+ Hồng +
+

+

15-12-2025, 10:10 am (0 ngày)

+

+
+
+ + xem +
+
1 + + +
+

[Hura]

+ Ngọc +
+

15-12-2025, 10:10 am (0 ngày)

+
+
+ + xem +
+
1 + + +
+

[KH]

+ Kh@gmail.com +
+

15-12-2025, 10:10 am (0 ngày)

+
+
+ + xem +
+
+
+ + + + + + +
+ + +
+
+ Liên kết nhanh + +
+ +
+ + +
+
+ +
+ + +
+
\ No newline at end of file diff --git a/template/javascript/brand.html b/template/javascript/brand.html new file mode 100644 index 0000000..53b156a --- /dev/null +++ b/template/javascript/brand.html @@ -0,0 +1,40 @@ + + + + \ No newline at end of file diff --git a/template/javascript/edit_template.html b/template/javascript/edit_template.html new file mode 100644 index 0000000..a566d74 --- /dev/null +++ b/template/javascript/edit_template.html @@ -0,0 +1,83 @@ + + + + + \ No newline at end of file diff --git a/template/javascript/global.html b/template/javascript/global.html new file mode 100644 index 0000000..e69de29 diff --git a/template/javascript/homepage.html b/template/javascript/homepage.html new file mode 100644 index 0000000..11f4232 --- /dev/null +++ b/template/javascript/homepage.html @@ -0,0 +1,408 @@ + + + + + + + \ No newline at end of file diff --git a/template/javascript/index.html b/template/javascript/index.html new file mode 100644 index 0000000..5c70fd4 --- /dev/null +++ b/template/javascript/index.html @@ -0,0 +1,63 @@ + + + + + + + + + + + +{% if global.module == 'home' %} + +{% include javascript/homepage %} + +{% elsif global.module == 'product' %} + +{% if global.view == 'home' %} +{% include javascript/product_list %} +{% endif %} + +{% include javascript/product_form %} + +{% elsif global.module == 'deal' %} + +{% include javascript/product_form %} + +{% elsif global.module == 'marketing' %} + +{% include javascript/marketing_form %} + +{% elsif global.module == 'brand' %} + +{% include javascript/brand %} + +{% elsif global.module == 'report' %} + +{% include javascript/visitor %} + +{% elsif global.module == 'system' %} + +{% include javascript/system %} + +{% elsif global.module == 'page' %} + +{% include javascript/page %} + +{% elsif global.module == 'template' and global.view == 'edit-template' %} + +{% include javascript/edit_template %} + +{% elsif global.module == 'tag' and global.view == 'add' %} + +{% include javascript/tag %} + +{% elsif global.module == 'shipping2' %} + +{% include javascript/shipping2 %} + + +{% endif %} \ No newline at end of file diff --git a/template/javascript/marketing_form.html b/template/javascript/marketing_form.html new file mode 100644 index 0000000..ce6c1be --- /dev/null +++ b/template/javascript/marketing_form.html @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/template/javascript/page.html b/template/javascript/page.html new file mode 100644 index 0000000..0194efd --- /dev/null +++ b/template/javascript/page.html @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/template/javascript/product_form.html b/template/javascript/product_form.html new file mode 100644 index 0000000..027b982 --- /dev/null +++ b/template/javascript/product_form.html @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/template/javascript/product_list.html b/template/javascript/product_list.html new file mode 100644 index 0000000..5211f70 --- /dev/null +++ b/template/javascript/product_list.html @@ -0,0 +1,137 @@ + \ No newline at end of file diff --git a/template/javascript/shipping2.html b/template/javascript/shipping2.html new file mode 100644 index 0000000..fa1487d --- /dev/null +++ b/template/javascript/shipping2.html @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/template/javascript/system.html b/template/javascript/system.html new file mode 100644 index 0000000..4305b4b --- /dev/null +++ b/template/javascript/system.html @@ -0,0 +1,73 @@ + + + + + + + \ No newline at end of file diff --git a/template/javascript/tag.html b/template/javascript/tag.html new file mode 100644 index 0000000..55b57d4 --- /dev/null +++ b/template/javascript/tag.html @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/template/javascript/visitor.html b/template/javascript/visitor.html new file mode 100644 index 0000000..f40f9d0 --- /dev/null +++ b/template/javascript/visitor.html @@ -0,0 +1,285 @@ + + \ No newline at end of file diff --git a/template/job/applicants.html b/template/job/applicants.html new file mode 100644 index 0000000..c67b220 --- /dev/null +++ b/template/job/applicants.html @@ -0,0 +1,97 @@ +
+
+

Danh sách hồ sơ tuyển dụng

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ngày Ứng viên Ghi chú Quản trị Lựa chọn
1 + 09-03-2024 + +

- Họ tên: Hura test

+

- Email: test1@hurasoft.com

+

- Điện thoại: 0987654321

+
+ test + +

+ Đã xử lý

+

bởi test 09-03-2024, 11:01 am

+
+
+ Chi + tiết + + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+ +
+
\ No newline at end of file diff --git a/template/job/applicants_detail.html b/template/job/applicants_detail.html new file mode 100644 index 0000000..95008b7 --- /dev/null +++ b/template/job/applicants_detail.html @@ -0,0 +1,75 @@ +
+ + +
+
+

Chi tiết hồ sơ

+

Phụ trách Chi nhánh Công ty cổ phần Traphaco tại Đồng Nai

+
+ +
+
+ +
+
+
+ Thông tin ứng viên + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Việc ứng tuyểnPhụ trách Chi nhánh Công ty cổ phần Traphaco tại Đồng Nai
Họ tênHura test
Địa chỉ emailtest1@hurasoft.com
Số điện thoại0987654321
File CVtest.docx
Thời gian đăng ký09-03-2024, 10:55 am
Tình trạng xử lý +
+

+ Đã xử lý +

+

bởi test 09-03-2024, 11:01 am

+
+
+ + Cập nhật ghi chú + + + +
+
+
+
+
\ No newline at end of file diff --git a/template/job/form.html b/template/job/form.html new file mode 100644 index 0000000..a98dfa4 --- /dev/null +++ b/template/job/form.html @@ -0,0 +1,305 @@ + + +
+
+
+ {% if global.url contains 'id=' %} +

Sửa nội dung

+ +

+ Phụ trách chi nhánh công ty cổ phần traphaco tại đồng nai +

+ {% else %} +

Thêm mới

+ {% endif %} + + +
+ +
+ +
+ +
+
+

Thông tin cơ bản

+ +
+

Ảnh đại diện

+ +
+ +
+ + +
+ +
+

Ảnh nền trong chi tiết

+ +
+ +
+ + +
+ +
+

Danh mục * có thể chọn nhiều danh + mục 

+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+

Tiêu đề

+ + +
+ +
+

Link Index

+ + +
+ +
+

Link tham khảo (link web ngoài nếu có)

+ + +
+ +
+

Tóm tắt chính

+ + +
+ +
+

Tags (Mỗi cụm từ 1 dòng)

+ + +
+
+ + +
+
+

Mô tả  công việc

+
+ +
+ +
+ +
+

Thu nhập + V.d Từ 8 triệu/ + Thỏa thuận

+ + +
+ + +
+
+

Số lượng cần tuyển + Nhập số lượng v.d. 2

+ + +
+
+

Hạn ứng tuyển + V.d Từ 8 triệu/ + Thỏa thuận

+ +
+ + +
+
+
+ +
+

Địa điểm

+ + +
+
+ + +
+

Dùng cho SEO

+ + +
+

+ Meta Title +  0 ký tự , 0 từ +

+ + +
+ +
+

+ Meta Keywords +  0 ký tự , 0 từ +

+ + +
+ +
+

+ Meta Description + 0 ký tự , 0 từ (Khuyến nghị: 160 ký tự) +

+ + +
+ +
+

Trạng thái

+ +
+ + + + + + +
+
+ +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/template/job/home.html b/template/job/home.html new file mode 100644 index 0000000..7f2b1cd --- /dev/null +++ b/template/job/home.html @@ -0,0 +1,343 @@ +
+
+

Tuyển dụng

+ +
+ + +
+ +
+
+ + + + + Xem nội dung đang ẩn + + + + Xem nội dung nổi bật + +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Danh sách Số liệu Quản trị Lựa chọn
1 + + + Phụ trách Chi nhánh Công ty cổ phần Traphaco tại Đồng Nai +
+ STT + +
+
+

- Số hồ sơ: 1

+

- Lượt xem: 0

+
+

Cập nhật: Admin

+

(15-02-2024, 2:57 pm)

+
+
+ + + + + + + + + + + + + + +
+
1 + + + Phụ trách Chi nhánh Công ty cổ phần Traphaco tại Đồng Nai +
+ STT + +
+
+

- Số hồ sơ: 1

+

- Lượt xem: 0

+
+

Cập nhật: Admin

+

(15-02-2024, 2:57 pm)

+
+
+ + + + + + + + + + + + + + +
+
1 + + + Phụ trách Chi nhánh Công ty cổ phần Traphaco tại Đồng Nai +
+ STT + +
+
+

- Số hồ sơ: 1

+

- Lượt xem: 0

+
+

Cập nhật: Admin

+

(15-02-2024, 2:57 pm)

+
+
+ + + + + + + + + + + + + + +
+
1 + + + Phụ trách Chi nhánh Công ty cổ phần Traphaco tại Đồng Nai +
+ STT + +
+
+

- Số hồ sơ: 1

+

- Lượt xem: 0

+
+

Cập nhật: Admin

+

(15-02-2024, 2:57 pm)

+
+
+ + + + + + + + + + + + + + +
+
1 + + + Phụ trách Chi nhánh Công ty cổ phần Traphaco tại Đồng Nai +
+ STT + +
+
+

- Số hồ sơ: 1

+

- Lượt xem: 0

+
+

Cập nhật: Admin

+

(15-02-2024, 2:57 pm)

+
+
+ + + + + + + + + + + + + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+ +
+
\ No newline at end of file diff --git a/template/marketing/conditional_promotion.html b/template/marketing/conditional_promotion.html new file mode 100644 index 0000000..016cb82 --- /dev/null +++ b/template/marketing/conditional_promotion.html @@ -0,0 +1,79 @@ +
+
+

Khuyến mại cấu hình máy tính

+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Khuyến mại Điều kiện áp dụng Thông tin khác Lựa chọn
1 +

20.000.000

+

- Tặng tiền mặt: 500.000

+

- Tùy chọn gói sản phẩm: 500.000

+

- Nội dung:Tặng ngay tiền mặt 500.000 VNĐ

+
+

1. Bắt buộc phải có sản phẩm thuộc các danh mục: 30,31,32,164,35,36

+

2. Giá trị đơn hàng trong khoảng: 20.000.000 VND - 30.000.000 VND

+
+

Tạo bởi: Nguyễn Tiến Dinh (10-11-2023, 10:06 am)

+

Cập nhật: Nguyễn Tiến Dinh (18-12-2023, 2:59 pm)

+
+
+ + + + + + + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
+
\ No newline at end of file diff --git a/template/marketing/conditional_promotion_add.html b/template/marketing/conditional_promotion_add.html new file mode 100644 index 0000000..25e5bad --- /dev/null +++ b/template/marketing/conditional_promotion_add.html @@ -0,0 +1,141 @@ + + +
+
+

Cập nhật khuyến mại

+ + + + + Thêm khuyến mại + + +

+ 100 triệu +

+
+ + + +
+ + + + + + + + + + + + + + + + + + + + +
Tên gọi + + * Phần này chỉ dùng cho quản trị viên phân biệt. +
Nội dung khuyến mại + +
+

1. Tặng tiền mặt: VND

+ +
+ +
+

2. Tùy chọn gói sản phẩm: VND

+ +
+ +
+

3. Nội dung:

+ +
+ +
Điều kiện áp dụng * + +
+

1. Bắt buộc phải có sản phẩm thuộc các danh mục:

+ +
+ +

2. Giá trị đơn hàng trong khoảng: VND +

+ +
+
+

Giá từ

+ +
+ +
+

Đến

+ +
+
+ +
Hiển thị + + + + +
+ + +
+
+ + + + + + + \ No newline at end of file diff --git a/template/marketing/coupon.html b/template/marketing/coupon.html new file mode 100644 index 0000000..b2a0310 --- /dev/null +++ b/template/marketing/coupon.html @@ -0,0 +1,157 @@ +
+
+

Coupon khuyến mại

+ +
+

Danh sách phiếu giảm giá/ quà tặng dùng cho khách hàng khi đặt mua hàng từ + website. + Trước khi gửi đơn hàng, nếu khách hàng có mã số phiếu còn hiệu lực thì sẽ nhập vào đơn hàng để nhận được + ưu + đãi ghi trên phiếu

+ + + + + Thêm phiếu mới + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Mã số Khuyến mại Phân loại Thời gian Trạng thái Thông tin khác Lựa chọn
1 CP231020233 +

Test phiếu giảm giá

+
Test phiếu giảm giá
+
+

Tặng sản phẩm

+

Balo thời trang

+
+ +
+

+ - Bắt đầu: 23-10-2023, 12:00 am
+ - Kết thúc: 28-10-2023, 12:00 am +

+
+ + Đã chạy + + + + Đã dừng + +
+
+
+

0 đã dùng

+

Tổng số: 20

+
+
+

- Tạo bởi: Đức (05-09-2022, 9:48 am)

+

- Cập nhật cuối: hongvt@hurasoft.com
(03-10-2023, 8:31 am)

+

- Giá trị đơn hàng tối thiểu: 2.000.000 đ +

+
+
+
+ + + + + + + + +
+
1 CP231020233 +

Test phiếu giảm giá

+
Test phiếu giảm giá
+
+

Tặng sản phẩm

+

Balo thời trang

+
+
+

+ - Bắt đầu: 23-10-2023, 12:00 am
+ - Kết thúc: 28-10-2023, 12:00 am +

+
+ + Đã chạy + + + + Đã dừng + +
+
+
+

0 đã dùng

+

Tổng số: 20

+
+

- Tạo bởi: Đức (05-09-2022, 9:48 am)

+

- Cập nhật cuối: hongvt@hurasoft.com
(03-10-2023, 8:31 am)

+

- Giá trị đơn hàng tối thiểu: 2.000.000 đ

+
+
+ + +
+
+
+
+
\ No newline at end of file diff --git a/template/marketing/coupon_add.html b/template/marketing/coupon_add.html new file mode 100644 index 0000000..80efea5 --- /dev/null +++ b/template/marketing/coupon_add.html @@ -0,0 +1,668 @@ + + +
+
+ {% if global.url contains 'id=' %} +
+

Cập nhật

+ + + + + Thêm phiếu mới + + +

+ Giảm 10.000đ khi mua đặt hàng online (nhập mã test2023) +

+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Mã số phiếu * CP220920213
Tên phiếu khuyến mại * + + * Phần này sẽ hiển thị trong đơn hàng của khách hàng, + do vậy cần viết cụ thể. +
Mô tả + + + Phần này chỉ dành cho admin nắm được cụ thể khuyến mại + này làm gì. Khách hàng không nhìn thấy. +
Phân loại * +
+ + +
+ Tên Sản phẩm + +
+ + + + +
+
Thời gian hiệu lực * + +
+

Bắt đầu

+
+ + +
+ +

Giờ

+ + +
+ +
+

Kết thúc

+
+ + +
+ +

Giờ

+ + +
+
Giá trị đơn hàng tối thiểu + + vnd (để 0 nếu không áp dụng) +
Giới hạn số lần sử dụng / 1 khách hàng + + (để 0 nếu không giới hạn số lượng dùng phiếu này trên 1 khách hàng) +
Áp dụng đồng thời với các mã KM khác + +
Hiển thị công khai + +
Điều kiện áp dụng + +
+ +
Số lượng phiếu + + (để 0 nếu không có giới hạn) +
Hiển thị + + + + +
+ + + +
+ + {% else %} + +

+ Thêm / sửa coupon

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Mã số phiếu * + + + + + + + +
Tên phiếu khuyến mại * + + * Phần này sẽ hiển thị trong đơn hàng của khách hàng, + do vậy cần viết cụ thể. +
Mô tả + + + Phần này chỉ dành cho admin nắm được cụ thể khuyến mại + này làm gì. Khách hàng không nhìn thấy. +
Phân loại * +
+ + +
+ Tên Sản phẩm + +
+ + + + +
+
Thời gian hiệu lực * + +
+

Bắt đầu

+
+ + +
+ +

Giờ

+ + +
+ +
+

Kết thúc

+
+ + +
+ +

Giờ

+ + +
+
Giá trị đơn hàng tối thiểu + + vnd (để 0 nếu không áp dụng) +
Giới hạn số lần sử dụng / 1 khách hàng + + (để 0 nếu không giới hạn số lượng dùng phiếu này trên 1 khách hàng) +
Áp dụng đồng thời với các mã KM khác + +
Hiển thị công khai + +
Điều kiện áp dụng + +
+ + +
Số lượng phiếu + + (để 0 nếu không có giới hạn) +
Hiển thị + + + + +
+ + + +
+ + {% endif %} +
+
+ + + \ No newline at end of file diff --git a/template/marketing/coupon_product.html b/template/marketing/coupon_product.html new file mode 100644 index 0000000..f1813a0 --- /dev/null +++ b/template/marketing/coupon_product.html @@ -0,0 +1,116 @@ + + +
+
+
+

Cập nhật

+ + + + + Thêm phiếu mới + + +

+ Giảm 10.000đ khi mua đặt hàng online (nhập mã test2023) +

+
+ + + +
+ +
+

Danh sách sản phẩm áp dụng phiếu giảm giá

+ + + + + Thêm sản phẩm mới + +
+ + +
+ + +

Chọn danh mục

+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Tên Thông tin bán hàng Lựa chọn
1 + + + Mainboard MSI B450 TOMAHAWK MAX II (AMD B450, Socket AM4, ATX, 4 Khe Cắm Ram DDR4) + +

- Mã kho: CN1

+

- Giá bán: 1.999.000 vnđ

+

- Kho hàng : 1

+

- Bảo hành : 36 Tháng

+
+
+ + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
+
+
\ No newline at end of file diff --git a/template/marketing/detail.html b/template/marketing/detail.html new file mode 100644 index 0000000..281c686 --- /dev/null +++ b/template/marketing/detail.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/template/marketing/feed.html b/template/marketing/feed.html new file mode 100644 index 0000000..58872d1 --- /dev/null +++ b/template/marketing/feed.html @@ -0,0 +1,108 @@ +{% if global.url contains 'view=fb-product-select' %} + +{% include 'marketing/feed_select' %} + +{% elsif global.url contains 'view=fb-product-add' %} + +{% include 'marketing/feed_add' %} + +{% elsif global.url contains 'view=fb-product-list' %} + +{% include 'marketing/feed_list' %} + +{% else %} +
+
+

Facebook Product Ads

+ +
+

Tổng hợp các danh sách chứa Sản phẩm dùng quảng cáo trên Facebook. Dùng link Facebook + Feed + ở danh sách phù hợp để nhập trong facebook ad manager

+ + + + + Thêm danh sách mới + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Danh sách Sản phẩm Facebook feed Google feed Cập nhật Lựa chọn
1 2023 còn hàng 6.055 sp +
+ + FB feed +
+
+ + +

Trương Thị Thái Ninh

+

25-10-2023, 3:05 pm

+
+
+ Xem + sản phẩm + + + + + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
+
+{% endif %} \ No newline at end of file diff --git a/template/marketing/feed_add.html b/template/marketing/feed_add.html new file mode 100644 index 0000000..9fffcb2 --- /dev/null +++ b/template/marketing/feed_add.html @@ -0,0 +1,70 @@ + + +
+
+
+

Cập nhật nhóm FB ad

+

ALL SP WEB 2023 NEW

+
+ + + +
+
+

Thông tin cơ bản

+ + + Chọn Sản phẩm vào danh sách + +
+ +
+ Đặt tên danh sách + + +
+ + +
+

Ghi chú / mô tả

+ + +
+ + +
+
+

+ Google Product Category ID + Chỉ điền 1 danh mục: ID hoặc Tên danh mục trong danh sách  + tại đây +

+ + V.d nếu là sp Bird Food thì điền 4990 hoặc Animals & Pet Supplies > + Pet Supplies > Bird Supplies > Bird Food +
+ + +
+ +
+ + +
+
+
\ No newline at end of file diff --git a/template/marketing/feed_list.html b/template/marketing/feed_list.html new file mode 100644 index 0000000..62f2731 --- /dev/null +++ b/template/marketing/feed_list.html @@ -0,0 +1,113 @@ + + +
+

Danh sách: 2023 còn hàng

+ +
+ + +
+ +
+ + Xóa danh sách được chọn + + +
+ + + + + + Thêm sản phẩm + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Tên Sản phẩm Thông tin bán hàng Lựa chọn
+ + 1 + + + Chuột game Steelseries Prime Mini (62421) (USB/RGB) +

Mã kho:  MOVI213

+

Lượt xem: 3360

+

Cập nhật: Hôm nay, 8:59 am

+ +

Chưa hiển thị

+
+

Giá: 999.000 vnđ

+

Kho hàng: 5

+

Bảo hành: 36 Tháng

+
+ + Loại sản phẩm này + +
2 Chuột game Steelseries

Mã kho:  MOVI213

Lượt xem: 3360

Cập nhật: Hôm nay, 8:59 am

Chưa hiển thị

Giá: 999.000 vnđ

Kho hàng:

Bảo hành:

Loại sản phẩm này
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
+ + + + \ No newline at end of file diff --git a/template/marketing/feed_select.html b/template/marketing/feed_select.html new file mode 100644 index 0000000..3faa3ce --- /dev/null +++ b/template/marketing/feed_select.html @@ -0,0 +1,83 @@ + + +
+

Chọn sản phẩm hiển thị Facebook

+ +
+ + +
+ +
+ + +
+ + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Tên Sản phẩm Thông tin bán hàng Lựa chọn
1 + Chuột game Steelseries Prime Mini (62421) (USB/RGB) +

Mã kho: MOVI213

+

Lượt xem: 3.360

+

Cập nhật: Hôm nay, 8:59 am

+
+

Giá: 999.000 vnđ

+

Kho hàng: 5

+

Bảo hành: 36 Tháng

+
+ Chọn sản phẩm +
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
\ No newline at end of file diff --git a/template/marketing/home.html b/template/marketing/home.html new file mode 100644 index 0000000..281c686 --- /dev/null +++ b/template/marketing/home.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/template/marketing/poster_list.html b/template/marketing/poster_list.html new file mode 100644 index 0000000..5c542d3 --- /dev/null +++ b/template/marketing/poster_list.html @@ -0,0 +1,45 @@ +
+

Danh sách poster quảng cáo

+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Quản trị Tên file Danh sách file Thời gian đăng Lựa chọn
1 admin@hurasoft.com Huyen - Test - Bỉm (Mẹ & Bé).html 02-08-2023, 4:51 pm
2 admin@hurasoft.com Huyen - Test - Bỉm (Mẹ & Bé).html 02-08-2023, 4:51 pm
+
+
\ No newline at end of file diff --git a/template/marketing/poster_upload.html b/template/marketing/poster_upload.html new file mode 100644 index 0000000..c68f061 --- /dev/null +++ b/template/marketing/poster_upload.html @@ -0,0 +1,48 @@ + + +
+ + Chức năng này cho phép bạn upload các file quảng cáo poster. + Yêu cầu cần đặt trong file đuôi .zip. Server này giới hạn dung lượng file upload là 20 MB, do vậy nếu file .zip + dung lượng lớn hơn 20MB thì bạn cần sử dụng công cụ FTP (hoặc liên hệ bên cung cấp server để nâng giới hạn file + upload). + +

+ Upload poster quảng cáo

+ +
+ + +
+

File zip chỉ được phép chứa file ảnh (jpg, gif) và html nếu có.

+ +

+ Chú ý: Bạn cần nhớ tên file để truy cập, ví dụ bạn upload file abc.zip có + chứa file khuyenmai.html, thì sau khi upload xong, đường dẫn truy cập sẽ là + https://demopc8.hurasoft.com/media/marketing/abc/khuyenmai.html +

+ +
+

Chọn file zip để upload (dung lượng tối đa 20MB)

+ +
+ + +
+
+
\ No newline at end of file diff --git a/template/marketing/product_offer.html b/template/marketing/product_offer.html new file mode 100644 index 0000000..0979318 --- /dev/null +++ b/template/marketing/product_offer.html @@ -0,0 +1,63 @@ +
+
+ +
+

Sản phẩm phiếu khuyến mại

+ + + + Tải danh sách hiện tại + + +

Hướng dẫn: Để cập nhật/xoá Sản phẩm có mã khuyến mại, tải file + excel mới nhất về và sửa từ đó. Để xoá sp thì chỉ cần xoá sp khỏi file excel. Sau khi chỉnh xong file + excel thì Upload lại để hệ thống nhận mới.

+
+ + +
+ +
+ + +
+ + + + + + + + + + + + + +
File cập nhật + +
Lựa chọn + + + + (* Nếu lựa chọn, hệ thống sẽ xóa bỏ hết dữ liệu đang có trước khi + cập nhật dữ liệu nhận được từ file excel) +
+ + +
+
+
\ No newline at end of file diff --git a/template/marketing/promotion.html b/template/marketing/promotion.html new file mode 100644 index 0000000..4a64934 --- /dev/null +++ b/template/marketing/promotion.html @@ -0,0 +1,88 @@ +
+
+

Khuyến mại theo sản phẩm

+ +
+ + +
+ + +
+
+ +
+

Danh sách các khuyến mại đi kèm khi người dùng mua Sản phẩm. Bạn cần tạo khuyến mại và lựa chọn Sản phẩm + phù + hợp cho từng khuyến mại

+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STT Khuyến mại Thời gian Cập nhật cuối Sản phẩm Lựa chọn
1 +

+ [Quà tặng] Phiếu mua hàng trị giá 100.000 VND +

+

- Mô tả: Phiếu mua hàng

+

- Giá trị: 0 VND

+

- Số lượng: 0

+
+

Không giới hạn thời gian

+
+

Admin

+

06-06-2020, 10:57 am

+
+ Xem + + +
+
+
+
\ No newline at end of file diff --git a/template/marketing/promotion_add.html b/template/marketing/promotion_add.html new file mode 100644 index 0000000..2e9cb37 --- /dev/null +++ b/template/marketing/promotion_add.html @@ -0,0 +1,257 @@ + + +
+
+

Cập nhật

+ + + + Thêm khuyến mại mới + + +

+ Tặng phiếu vệ sinh bảo dưỡng Laptop, PC miễn phí trọn đời trị giá 999.000đ (THEK417) +

+
+ +
+ + + +
+ +

Thông tin cơ bản

+ +
+ Hình thức + +
+ +
+ Ảnh đại diện (nếu có) +
+
+ +
+ Đặt tiêu đề khuyến mại + +
+ +
+
+

Giá trị bằng (VNĐ)

+ + +
+
+

Số lượng (Để = -1 nếu không hạn chế số lượng)

+ +
+
+ +
+ Mô tả + + +
+ +
+ Thời gian hiệu lực + +
+ + + + +
+
+

Bắt đầu

+ +
+
+

Giờ

+ +
+
+
+
+

Kết thúc

+ +
+
+

Giờ

+ +
+
+
+
+ +
+
Link liên quan (nếu có) (Link tới 1 bài viết giới thiệu chi + tiết hoặc link + sản phẩm)
+ +
+ +
+

Ghi chú

+ + +
+ +
+

Hiển thị

+ + + + + + +
+ +

+ + +
+
+ +
\ No newline at end of file diff --git a/template/marketing/promotion_group.html b/template/marketing/promotion_group.html new file mode 100644 index 0000000..df0e4ee --- /dev/null +++ b/template/marketing/promotion_group.html @@ -0,0 +1,84 @@ + + +
+

Nhóm khuyến mại

+ +
+

Tổ chức khuyến mại theo nhóm để lựa chọn cho sản phẩm nhanh hơn.

+ + + + + Thêm nhóm mới + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Nhóm Danh sách khuyến mại Ghi chú Cập nhật cuối Sản phẩm Lựa chọn
1 + KM màn +

- Mô tả:

+

- Phân loại: Khách hàng được nhận tất cả KM trong nhóm

+
+

- Đón Tết Rồng Vàng Tặng 2024 Phần Quà (0 VND)

+

- KM Giá treo màn hình (0 VND)

+
+

Trần Kim Ngân

+

05-02-2024, 10:15 am

+
+ + Xem sản phẩm + + +
+ + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+ +
diff --git a/template/marketing/promotion_group_add.html b/template/marketing/promotion_group_add.html new file mode 100644 index 0000000..4f60f33 --- /dev/null +++ b/template/marketing/promotion_group_add.html @@ -0,0 +1,187 @@ + + +
+
+ {% if global.url contains 'id=' %} +
+

Cập nhật

+ + + + + Thêm nhóm mới + + +

+ Tặng phiếu vệ sinh bảo dưỡng Laptop, PC miễn phí trọn đời trị giá 999.000đ (THEK417) +

+
+ + + {% endif %} + +
+

Thông tin cơ bản

+ +
+

Đặt tên nhóm

+ + +
+ +
+

Mô tả

+ + +
+ +
+

Chọn khuyến mại

+ + + Chọn khuyến mại vào nhóm + + + (* Cần nhấn Cập nhật để lưu lại) +
+ +
+ + + + + + + + + + + + + + + + + + + +
Khuyến mại Lựa chọn
- Tặng ngay 01 Bếp lẩu nướng đa năng- hiệu Edra +
+ + + + + + +
+
- Test Khuyến mại 2
- Test 3
+
+ + +
+

Ghi chú (* dành cho quản trị)

+ + +
+ +
+ + + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/template/marketing/promotion_group_product.html b/template/marketing/promotion_group_product.html new file mode 100644 index 0000000..f55f035 --- /dev/null +++ b/template/marketing/promotion_group_product.html @@ -0,0 +1,290 @@ + + +
+
+ {% if global.url contains 'id=' %} +
+

Cập nhật

+ + + + + Thêm nhóm mới + + +

+ Tặng phiếu vệ sinh bảo dưỡng Laptop, PC miễn phí trọn đời trị giá 999.000đ (THEK417) +

+
+ + + {% endif %} + +
+
+

Danh sách sản phẩm có nhóm khuyến mại

+ + + + + Thêm sản phẩm mới + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Tên Thông tin bán hàng Lựa chọn
1 + + + Mainboard MSI B450 TOMAHAWK MAX II (AMD B450, Socket AM4, ATX, 4 Khe Cắm Ram DDR4) +

Mã kho : Hurasoft212333

+
+

- Giá bán: 1.999.000 vnđ

+

- Kho hàng : 1

+

- Bảo hành : 36 Tháng

+
+
+ + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/template/marketing/promotion_product.html b/template/marketing/promotion_product.html new file mode 100644 index 0000000..1d0daeb --- /dev/null +++ b/template/marketing/promotion_product.html @@ -0,0 +1,290 @@ + + +
+
+ {% if global.url contains 'id=' %} +
+

Cập nhật

+ + + + + Thêm khuyến mại mới + + +

+ Tặng phiếu vệ sinh bảo dưỡng Laptop, PC miễn phí trọn đời trị giá 999.000đ (THEK417) +

+
+ + + {% endif %} + +
+
+

Danh sách sản phẩm có nhóm khuyến mại

+ + + + + Thêm sản phẩm mới + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Ảnh Tên Thông tin bán hàng Lựa chọn
1 + + + Mainboard MSI B450 TOMAHAWK MAX II (AMD B450, Socket AM4, ATX, 4 Khe Cắm Ram DDR4) +

Mã kho : Hurasoft212333

+
+

- Giá bán: 1.999.000 vnđ

+

- Kho hàng : 1

+

- Bảo hành : 36 Tháng

+
+
+ + + +
+
+
+ +
+ + 1 + 2 + 3 + 4 + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/template/media/category.html b/template/media/category.html new file mode 100644 index 0000000..7eba9e3 --- /dev/null +++ b/template/media/category.html @@ -0,0 +1,52 @@ +
+ + + +
+

Phân loại nhóm danh mục media

+ + + + + Thêm nhóm mới + + + + + + + + + + + + + + + + + + + + + +
STTIDTên nhómThời gianLựa chọn
11385Ảnh tin tức2023-10-01 12:00:00 +
+ + +
+
+ +
+
\ No newline at end of file diff --git a/template/media/home.html b/template/media/home.html new file mode 100644 index 0000000..dab7d42 --- /dev/null +++ b/template/media/home.html @@ -0,0 +1,102 @@ +
+
+

Danh sách media

+ +
+ + +
+ +
+
+ + + + Quản lý nhóm media + +
+ + + + + Thêm nội dung mới + +
+ +

+ Hướng dẫn: Chức năng này cho phép bạn quản lý tất cả các loại file media (như file word, excel, ảnh + chương trình, ảnh banner, file flash, ảnh Sản phẩm ...) phục vụ cho website. Copy đường dẫn file và gắn vào + miêu + tả Sản phẩm hay bài viết khác. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Xem nhanh Tên file Loại file Thời gian Lựa chọn
1 + + Xem nhanh + + +
+

Đường dẫn

+ + +
+
banner 2023-08-11 00:00:00 +
+ + + +
+ +
+

STT

+ + +
+
+
+ +
+
\ No newline at end of file diff --git a/template/media/upload.html b/template/media/upload.html new file mode 100644 index 0000000..f1fccbc --- /dev/null +++ b/template/media/upload.html @@ -0,0 +1,129 @@ +
+ + + +
+
+
+

Cập nhật media

+ + + + Thêm file Media mới + +
+

/media/lib/11-08-2023/new.png

+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Link file + +
Đặt tên + +
Phân nhóm +
+ + + Quản lý nhóm media + +
+
Chiều rộng (width) +
+ + (điền số cho đơn vị pixel, bắt buộc với + Flash, Video + clip) +
+
Chiều Cao (height) +
+ + (điền số cho đơn vị pixel, bắt buộc với + Flash, Video + clip) +
+
File ảnh + +
+

Upload file

+ +
+ Dung lượng file phải nhỏ hơn 20MB +
Code HTML + +
Hoặc Link trên mạng + +
+ + +
+
+
+ +
+ +
\ No newline at end of file diff --git a/template/order/bargain.html b/template/order/bargain.html new file mode 100644 index 0000000..0161374 --- /dev/null +++ b/template/order/bargain.html @@ -0,0 +1,99 @@ +
+

Danh sách mặc cả giá

+ + +

+ Tổng số mặc cả giá nhận được: 1.458 +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STT Thời gian Khách hàng Thông tin sản phẩm Trạng thái
1 16-11-2023, 9:59 am +

- Họ tên: Ngô Quang Sơn

+

- Email: sonnq57@yahoo.com.sg

+

- Tel: 0913236989

+
+

- Sản phẩm: Tai nghe NICEHCK Traceless có Mic (Type-C)

+

- Giá bán (lúc mặc cả): 258.850.000

+

- Giá mặc cả: 0

+

- Nguồn mặc cả: https://hacom.vn/

+
+
+ Thành công + Chưa xử lý + Hủy bỏ + + +
+ +

Lúc 28-02-2015, 12:40 pm

+
+ +
+
+ +
+ + + + 1 + 2 + 3 + 4 + 5 + + + +
+
+ + \ No newline at end of file diff --git a/template/order/detail.html b/template/order/detail.html new file mode 100644 index 0000000..d546bb0 --- /dev/null +++ b/template/order/detail.html @@ -0,0 +1,514 @@ + + +
+ +
+ +
+ Đơn hàng #000-368 + +
+

Thời gian: 16-11-2023, 9:59 am

+
+ +
+
+
+ 1. Khách hàng + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Tên Nguyen Duc Quan
Số điện thoại 0822256369
Email quannd@hurasoft.com
Địa chỉ nhận hàng + Kiến Hưng, Quận Hà Đông, Hà Nội + +
Địa chỉ thanh toán + Kiến Hưng, Quận Hà Đông, Hà Nội + +
+
+
+ + +
+ 2. Sản phẩm + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
STT Mã kho Tên Giá SL Tổng Ghi chú
1 HH0003 + HHWS - CORE i9 9900X | 32G | + NVIDIA RTX 2060 6GB + 34.800.000 VND (chiếc) 2 69.600.000 VND Ghi chú
+
+
+ + +
+ 3. Số tiền + +
+ + + + + + + + + + + + + + + + + + + + +
Tổng giá trị + 69.600.000 VND +
Phí vận chuyển & giao hàng + 0 VND + +
Phí thu hộ + 0 VND + +
Tổng thu 69.600.000 VND
+
+
+ + +
+ 4. Lịch sử cập nhật + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
STT Phân loại Trạng thái Ghi chú Người cập nhật
1 payment paid test Đức lúc 22-05-2023, 4:49 pm
Hiện tại chưa có trạng thái nào được cập nhật
+
+
+
+ +
+
+

Nhân sự xử lý đơn hàng

+

+ Uyên Hura + +

+ + +
+ +
+

Trạng thái thanh toán

+ +
+ + Chưa thanh toán + + +
+ + + +

Khách chọn: Thanh toán bằng VNPAY

+
+ +
+

Trạng thái vận chuyển

+ +

+ + Chưa chuyển + + +

+ + + +

Khách chọn: Giao hàng tiêu chuẩn

+
+ +
+

+ Trạng thái đơn hàng + +

+ + +
+ +
+

Ghi chú

+ + + +
+ + +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/template/order/detail_new.html b/template/order/detail_new.html new file mode 100644 index 0000000..e69de29 diff --git a/template/order/home.html b/template/order/home.html new file mode 100644 index 0000000..4597844 --- /dev/null +++ b/template/order/home.html @@ -0,0 +1,195 @@ +
+

Danh sách đơn hàng

+ + + + + +
+
+

+ Tổng số đơn hàng: 1.458 +

+ +

+ Tổng giá trị: 14.948.348.000 VNĐ +

+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Mã số Thời gian Khách hàng Tỉnh/TP Giá trị + + + + + + + + Xem
1 + 000-369 + 16-11-2023, 9:59 am + Nguyen Duc Quan + Hà Nội 69.600.000 +
Thành công
+
+
Chờ xử lý
+
+
Lỗi
+
+ Nguyen Duc Quan + +
+ + +
+
1 + 000-369 + 16-11-2023, 9:59 am + Nguyen Duc Quan + Hà Nội 69.600.000 +
Thành công
+
+
Chờ xử lý
+
+
Đang xử lý
+
+ Nguyen Duc Quan + +
+ + +
+
+
+ +
+ + + + 1 + 2 + 3 + 4 + 5 + + + +
+
\ No newline at end of file diff --git a/template/order/paygate.html b/template/order/paygate.html new file mode 100644 index 0000000..784d1da --- /dev/null +++ b/template/order/paygate.html @@ -0,0 +1,127 @@ +
+

Thanh toán qua cổng dịch vụ

+ + + +
+ + +
+

Từ ngày

+ + +
+
+

Đến ngày

+ +
+ + +
+ +
+
+

Tổng số: 8

+

Tổng giá trị: 800.000 VNĐ

+
+ + + + Tải file excel + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT ID Cổng Thời gian Khách hàng Thanh toán cho Giá trị Trạng thái
1 10 viettelpay 06-11-2022, 11:26 am +

- Họ tên: Ngô Quang Sơn

+

- Email: sonnq57@yahoo.com.sg

+

- Tel: 0913236989

+
[order] 123 100.000 + Thành công + + +
+
+ +
+ + + + 1 + 2 + 3 + 4 + 5 + + + +
+
+ + \ No newline at end of file diff --git a/template/order/wait_order.html b/template/order/wait_order.html new file mode 100644 index 0000000..66e870e --- /dev/null +++ b/template/order/wait_order.html @@ -0,0 +1,114 @@ +
+

Đặt chờ sản phẩm

+ + + +
+ + +
+ +
+

Danh sách các Sản phẩm khách hàng chờ đặt hàng

+ + + + Tải file excel + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STT Khách hàng Sản phẩm Nhận hàng tại Ghi chú Trạng thái
1 +

- Họ tên: Ngô Quang Sơn

+

- Email: sonnq57@yahoo.com.sg

+

- Tel: 0913236989

+
+ [Mới 100%] Dell Gaming G16 7620 (Intel core + i7-12700H, 16GB, 1TB, + RTX 3060 6GB, 16″ QHD+ 165Hz IPS) + 20 Láng Hạ, Đống Đa, Hà Nội Nhận hàng giờ hành chính +
+ Thành công + + + +
+ +

Lúc 28-02-2015, 12:40 pm

+
+ +
+
+ +
+ + + + 1 + 2 + 3 + 4 + 5 + + + +
+
+ + \ No newline at end of file diff --git a/template/other/header.html b/template/other/header.html new file mode 100644 index 0000000..a58944d --- /dev/null +++ b/template/other/header.html @@ -0,0 +1,388 @@ + \ No newline at end of file diff --git a/template/page/category.html b/template/page/category.html new file mode 100644 index 0000000..2db1b8b --- /dev/null +++ b/template/page/category.html @@ -0,0 +1,82 @@ +
+ + +
+

Danh mục nội dung cố định

+ + + + + Thêm danh mục mới + + + + + + + + + + + + + + + + + + + + + + + + + + +
Danh mụcIDSTTLựa chọn
1.Chính sách chung2016 + + + +
1.Chính sách chung2016 + + + +
+
+
\ No newline at end of file diff --git a/template/page/category_add.html b/template/page/category_add.html new file mode 100644 index 0000000..c451a38 --- /dev/null +++ b/template/page/category_add.html @@ -0,0 +1,123 @@ +
+ + +
+
+
+

Cập nhật danh mục

+ +
+

Chính sách chung

+
+ +
+
+ +
+
+
+ Thông tin cơ bản + +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ +
+ + + Upload ảnh + +

No file chosen

+
+
+ +
+ +
+ Dùng cho SEO + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+
+ +
+ +
\ No newline at end of file diff --git a/template/page/form.html b/template/page/form.html new file mode 100644 index 0000000..1de87c7 --- /dev/null +++ b/template/page/form.html @@ -0,0 +1,156 @@ +
+ + +
+
+
+

Cập nhật nội dung

+ +
+

Chương trình khuyến mãi tháng

+
+ +
+
+ +
+
+
+ Thông tin cơ bản + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + + Quản lý danh mục + +
+
+ +
+ +
+ + + Upload ảnh + +

No file chosen

+
+
+ +
+ +
+ Nội dung chi tiết +
+ + +
+ +
+

Hoặc nhập nội dung

+ +
+ + + + +
+ +
+ Dùng cho SEO + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+
+ +
+ +
\ No newline at end of file diff --git a/template/page/home.html b/template/page/home.html new file mode 100644 index 0000000..e06b6c2 --- /dev/null +++ b/template/page/home.html @@ -0,0 +1,187 @@ +
+
+

Danh sách nội dung cố định

+ + + +
+ Hướng dẫn : Phần này tổng hợp các nội dung cố định cung cấp cho khách hàng (v.d. các hướng dẫn mua + hàng, quy + định, chính + sách bán hàng, vv..) +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTDanh sáchThời gianLượt xemLựa chọn
1 +
+ [#188] HACOM Sinh nhật 22 tuổi cùng Acer +
+
+

URL

+ +
+
+ Cập nhật: 29-12-2023, 9:56 am + + 0 + + +
1 +
+ [#188] HACOM Sinh nhật 22 tuổi cùng Acer +
+
+

URL

+ +
+
+ Cập nhật: 29-12-2023, 9:56 am + + 0 + + +
1 +
+ [#188] HACOM Sinh nhật 22 tuổi cùng Acer +
+
+

URL

+ +
+
+ Cập nhật: 29-12-2023, 9:56 am + + 0 + + +
+ + + +
+
\ No newline at end of file diff --git a/template/payinstall/add_company.html b/template/payinstall/add_company.html new file mode 100644 index 0000000..1728265 --- /dev/null +++ b/template/payinstall/add_company.html @@ -0,0 +1,50 @@ +
+ + + +
+
+
+

Cập nhật công ty

+

HD Saigon

+
+
+
+
+ +
+
+
+ Thông tin cơ bản + +
+ + +
+ +
+ + +
+ +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/template/payinstall/add_period.html b/template/payinstall/add_period.html new file mode 100644 index 0000000..afc0ed9 --- /dev/null +++ b/template/payinstall/add_period.html @@ -0,0 +1,68 @@ +
+ + + +
+
+
+

Cập nhật thời gian

+ + + + Thêm thời gian + +
+

6 tháng

+
+
+
+ +
+
+
+ Thông tin cơ bản + +
+
+ +

Tháng (* ghi số, ví dụ 6)

+
+ +
+ +
+
+ +

% (* ghi số thập phân, ví dụ: 2.6 - tương đương 2.6%)

+
+ +
+ +
+ + +
+ +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/template/payinstall/add_upfront.html b/template/payinstall/add_upfront.html new file mode 100644 index 0000000..4008ebf --- /dev/null +++ b/template/payinstall/add_upfront.html @@ -0,0 +1,68 @@ +
+ + + +
+
+
+

Cập nhật % trả trước

+ + + + Thêm trả trước + +
+

HD Saigon

+
+
+
+ +
+
+
+ Thông tin cơ bản + +
+
+ +

(* dùng để hiển thị trên web, ví dụ ghi 20 %)

+
+ +
+ +
+
+ +

% (* ghi số, ví dụ: 20% thì ghi là 20)

+
+ +
+ +
+ + +
+ +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/template/payinstall/category.html b/template/payinstall/category.html new file mode 100644 index 0000000..2e559f5 --- /dev/null +++ b/template/payinstall/category.html @@ -0,0 +1,58 @@ +
+
+

Danh mục sản phẩm có trả góp

+ + + + + + + + + + + + + + + + + + +
Danh mụcSPLựa chọn
+

+ 1. Màn Hình Máy Tính +

+
+ + + +
+ +
+
\ No newline at end of file diff --git a/template/payinstall/home.html b/template/payinstall/home.html new file mode 100644 index 0000000..0b3dcd5 --- /dev/null +++ b/template/payinstall/home.html @@ -0,0 +1,280 @@ +
+
+

Cài đặt trả góp

+ + + + Thêm công ty + + + + + + + + + + + + + + + + + + + + + + +
STTCông tyThông tin trả góp
1 +

HD Saigon

+
+ + +
+
+
+
+
+ Trả trước + + + Thêm trả trước +
+
+ + + + + + + + + + + + +
20%
20%
+
+ + +
+
+ + + + + + + + + + + + +
20%
20%
+
+ + +
+
+
+
+
+
+ Thời hẹn trả góp + + + Thêm thời gian +
+
+ + + + + + + + + + + + +
20%
20%
+
+ + +
+
+ + + + + + + + + + + + +
20%
20%
+
+ + +
+
+
+
+
+
1 +

HD Saigon

+
+ + +
+
+
+
+
+ Trả trước + + + Thêm trả trước +
+
+ + + + + + + + + + + + +
20%
20%
+
+ + +
+
+ + + + + + + + + + + + +
20%
20%
+
+ + +
+
+
+
+
+
+ Thời hẹn trả góp + + + Thêm thời gian +
+
+ + + + + + + + + + + + +
20%
20%
+
+ + +
+
+ + + + + + + + + + + + +
20%
20%
+
+ + +
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/template/payinstall/order.html b/template/payinstall/order.html new file mode 100644 index 0000000..da8d22c --- /dev/null +++ b/template/payinstall/order.html @@ -0,0 +1,83 @@ +
+
+

Danh sách đơn đăng ký

+ +
+ + +
+ +

Tổng số: 16

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STTMã đơnNhà cung cấpSản phẩmĐiều khoảnKhách hàngThời gianLựa chọn
15yl0uuds52fOhdsaigon +
+ +

- SKU: GPSO037

+

- Giá: 1.599.000

+
+
+ +

- SKU: GPSO037

+

- Giá: 1.599.000

+
+
+

- Giá trả góp: 1690146

+

- Trả trước: 799500

+

- Lãi suất: 1.90

+

- Tháng: 6

+
+

- Tên: phạm thanh chiến

+

- Địa chỉ: nhà 04 ngõ 362 lê viết thuật , Xã Hưng Lộc

+

- Tỉnh: Nghệ An

+

- Email: phamthanhchien1808@gmail.com

+

- Điện thoại: 0968 296 041

+
05-03-2024, 12:58 am +
+ Chi + tiết + +
+
+
+
+
\ No newline at end of file diff --git a/template/payinstall/order_detail.html b/template/payinstall/order_detail.html new file mode 100644 index 0000000..6915df3 --- /dev/null +++ b/template/payinstall/order_detail.html @@ -0,0 +1,91 @@ +
+ + + +
+
+

Chi tiết đơn hàng trả góp

+

9dDMHsx5CBgG

+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Mã đơn9dDMHsx5CBgG
Nhà cung cấpalepay
Sản phẩm +
+ +

- SKU: GPSO037

+

- Giá: 1.599.000

+
+
+ +

- SKU: GPSO037

+

- Giá: 1.599.000

+
+
Điều khoản +

- Giá trả góp: 6520000

+

- Trả trước: 2500000

+

- Lãi suất: 0

+

- Tháng: 6

+
Khách hàng +

- Nghề nghiệp:

+

- Tên: TRAN QUI

+

- Địa chỉ: 39 Cao Lỗ, Phường 04

+

- Tỉnh: TP HCM

+

- Email: quitnp83@gmail.com

+

- Điện thoại: 0919843649

+

- Ngày sinh:

+

- Giới tính:

+

- CMND:

+

- Siêu thị duyệt hồ sơ:

+
Thời gian05-03-2024, 12:11 am
+
+
+
+
+
\ No newline at end of file diff --git a/template/pcbuilder/config_edit.html b/template/pcbuilder/config_edit.html new file mode 100644 index 0000000..2fdeabf --- /dev/null +++ b/template/pcbuilder/config_edit.html @@ -0,0 +1,73 @@ +
+ + + +
+
+
+

Chỉnh sửa cấu hình và quan hệ lệ thuộc

+

Hướng dẫn: Nhập từng dòng, mỗi dòng là 1 loại linh kiện theo + form mẫu sau:

+ ID danh mục | Tên gọi linh kiện | ID liên quan 1, ID liên quan 2, ID liên quan + 2 +
+
+
+
+ +
+
+
+

+ Ví dụ 1: Ví dụ 1: 30 | Bo mạch chủ | 61,36 +

+

(Trong đó: 30 là ID danh mục của Bo mạch + chủ, 61 là ID danh mục RAM, 36 là ID danh mục + CPU. Bo mạch chủ cần tương thích với RAM và CPU nên chọn ở đây) +

+

+ Ví dụ 2: 10 | Màn hình | +

+

(Trong đó: 10 là ID danh mục + của Màn hình máy tính. Và Màn máy tính không cần phụ thuộc với các linh kiện khác nên để + trống)

+ + + +
+
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/template/pcbuilder/home.html b/template/pcbuilder/home.html new file mode 100644 index 0000000..1092e30 --- /dev/null +++ b/template/pcbuilder/home.html @@ -0,0 +1,70 @@ +
+
+

Danh sách linh kiện

+ + Chỉnh sửa linh kiện + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTID danh mụcTên danh mụcDanh mục liên quanSố lượng Sản phẩm
12Bộ vi xử lý +
+
+ Mainboard - Bo Mạch Chủ + Tạo + quan hệ +
+
+ ,RAM - Bộ Nhớ Trong + Tạo + quan hệ +
+
+
+ 32 +
12Bộ vi xử lý +
+
+ Mainboard - Bo Mạch Chủ + Tạo + quan hệ +
+
+ ,RAM - Bộ Nhớ Trong + Tạo + quan hệ +
+
+
+ 32 +
+
+
\ No newline at end of file diff --git a/template/pcbuilder/part_relation.html b/template/pcbuilder/part_relation.html new file mode 100644 index 0000000..d0ef441 --- /dev/null +++ b/template/pcbuilder/part_relation.html @@ -0,0 +1,138 @@ +
+ + + +
+
+
+

Mối quan hệ của linh kiện

+

Chọn 1 Sản phẩm ở một danh mục để chọn các Sản phẩm ở liên quan ở danh mục + kia. +

+
+
+
+ +
+
+ CPU - Bộ Vi Xử Lý (#2) +
+ + +
+ +
+

Bộ lọc

+ + + + + + Lọc + sản phẩm +
+ +
+ Kết quả chọn: +

0 sản phẩm

+
+ + + + + + + + + + + + + + + + + + +
+ + STTẢnhSản phẩm (Tổng số: 132)
+ + 1 + + + Máy đo độ bóng Horiba IG-320 (gloss meter),góc đo 60 °, + khoảng đo 0.0-100.0, phân độ 0.1, bộ nhớ 99 phép đo +

+ Góc đo tiêu chuẩn 60 ° Khoảng đo 0-100 khoảng hiển thị 0-200 Kết quả đo chính + xác với độ lặp lại +/- 0,5 % trên toàn dải + đo Quá trình đo cho kết quả không bị ảnh hưởng bởi ánh sáng môi trường bên ngoài + Lấy trung bình đến 99 điểm dữ liệu Xem + các dữ liệu được lưu trữ trên màn hình hiển thị Vận hành thiết bị dễ dàng với 1 + nốt bấm +

+

+ Mã kho: + MOVI213 +

+

Giá: 5.000.000

+

SL: 1

+

Trạng thái: Đang hiển thị

+
+ +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + + + +
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/template/product/addon.html b/template/product/addon.html new file mode 100644 index 0000000..6ef4172 --- /dev/null +++ b/template/product/addon.html @@ -0,0 +1,57 @@ +
+

Danh sách sản phẩm/dịch vụ bán kèm sản phẩm

+ + + +
+ + + + + + + + + + + + + + + + + + + +
STTThông tinGiáSản phẩmCập nhật
1 + +

Ghi chú: KM màn hình 27 1

+

Tạo lúc: 04-06-2022, 8:28 am, cập nhật: 04-06-2022, 8:28 am

+
3.849.000 vndXem + SP +
+ + + + +
+
+
+
\ No newline at end of file diff --git a/template/product/addon_add.html b/template/product/addon_add.html new file mode 100644 index 0000000..73cdbb5 --- /dev/null +++ b/template/product/addon_add.html @@ -0,0 +1,87 @@ +
+ + + +
+
+
+

Cập nhật SP/ Dịch vụ bán kèm

+ + + Thêm dịch vụ mới + +
+
+ +
+
+ +
+
+
+ Thông tin sản phẩm + +
+ + +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ Hiển thị + + +
+ +
+
+ + +
+
+
+ +
+
\ No newline at end of file diff --git a/template/product/attribute.html b/template/product/attribute.html new file mode 100644 index 0000000..bba30d7 --- /dev/null +++ b/template/product/attribute.html @@ -0,0 +1,97 @@ +
+

Danh sách thuộc tính

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTTên hiển thịGiá trịDanh mục đang cóThứ tựTrạng tháiCập nhật
1phong-cachPhong cách5 + + + + + Đang hoạt động + +
+ + + +
+
1phong-cachPhong cách5 + + + + + Đang hoạt động + +
+ + + +
+
+
+ +
\ No newline at end of file diff --git a/template/product/attribute_add.html b/template/product/attribute_add.html new file mode 100644 index 0000000..1d2b590 --- /dev/null +++ b/template/product/attribute_add.html @@ -0,0 +1,214 @@ +
+ + + +
+
+
+

Cập nhật thuộc tính sản phẩm

+ + + Thêm thuộc tính mới + +
+
+ +
+
+
+ +
+
+
+
+ Thông tin cơ bản + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ Lựa chọn áp dụng +
+ + + + + +
+
+ +
+ Phân loại +
+ + + +
+
+ +
+ Thông tin cơ bản +
+ + + + + + + + + + + + + + + + + + + + + +
STTIDGiá trịMô tảThứ tự hiển thịLựa chọn
14939Nam + + +
+ + +
+
+
+
+ +
+
+ Thêm giá trị mới + (**) Dùng để sắp xếp Sản phẩm theo thuộc tính khi cần (v.d. + theo + dung + lượng ổ cứng laptop giảm + dần) + +
+
+ + +
+
+ + +
+
+
+ + +
+ Thêm + mới +
+ +
+ +
+
+ +
+
+
+ +
+
\ No newline at end of file diff --git a/template/product/brand.html b/template/product/brand.html new file mode 100644 index 0000000..c9089f9 --- /dev/null +++ b/template/product/brand.html @@ -0,0 +1,100 @@ +
+

Danh sách thương hiệu

+ +
+
+ Tìm theo chữ cái: + A (13) | + B (2) | + C (5) | + D (3) | + E (3) | + F (1) | + G (4) | + H (4) | + I (5) | + J (2) | + K (2) | + L (4) | + M (3) | + N (4) | + O (1) | + P (5) | + Q (1) | + S (8) | + T (5) | + V (2) | + W (2) | + X (1) | + Z (2) | +
+ + + Thêm thương hiệu mới + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STTIDThương hiệuLượt xemLogoSố Sản phẩmCài đặtCập nhật
1750 + 10MOONS + +

Cập nhật cuối: 0000-00-00 00:00:00

+
+ 0 + + + + + +
+

1

+ Xem + SP +
+
+
+ STT + +
+
+
+ + + + +
+
+
+
\ No newline at end of file diff --git a/template/product/brand_add.html b/template/product/brand_add.html new file mode 100644 index 0000000..a7b6212 --- /dev/null +++ b/template/product/brand_add.html @@ -0,0 +1,133 @@ +
+ + + +
+
+
+

Cập nhật Thương hiệu

+ + + Thêm thương hiệu mới + +
+
+ +
+
+ +
+
+
+ Thông tin cơ bản + +
+ + +
+ +
+ + +
+
+ + +
+ +
+ +
+ +
+
+
+ +
+
+ +

Chưa có thư viện ảnh cho phần miêu tả

+
+ +
+ +
+ +
+ Dùng cho SEO +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
\ No newline at end of file diff --git a/template/product/category.html b/template/product/category.html new file mode 100644 index 0000000..b282f00 --- /dev/null +++ b/template/product/category.html @@ -0,0 +1,59 @@ +
+

Danh sách sản phẩm

+ + +
+ + + + + + + + + + + + + + {{page.category_list}} + +
Danh mụcLink webIDXemSPSTTHiển thị nội dungThuộc tínhCập nhật
+
+ +

+ Lưu ý: Tổng sản phẩm ở danh mục mẹ được tính là tổng sản phẩm sản phẩm + của các danh mục con và danh mục mẹ. Bộ đếm không loại trừ sản phẩm trùng nhau (v.d. trường hợp 1 sản phẩm + thuộc cả 2 danh mục con thì sẽ được đếm 2 lần) +

+ + + + +
+ + + \ No newline at end of file diff --git a/template/product/category_form.html b/template/product/category_form.html new file mode 100644 index 0000000..2c014c4 --- /dev/null +++ b/template/product/category_form.html @@ -0,0 +1,275 @@ +
+ + + +
+
+
+

Cập nhật danh mục

+ + + Thêm danh mục mới + +
+
+ + + {% if page.update_status == 'success' %} +

Cập nhật thành công

+ {% endif %} + +
+
+ +
+
+
+ + + +
+ Thông tin cơ bản + +
+ + +
+ +
+ + +
+
+ +
+ +

Chưa có thư viện ảnh cho phần miêu tả

+
+ +
+ +
+
+ Nhập nội dung cố định (HTML) +
+ +
+

Chưa có thư viện ảnh cho phần miêu tả

+
+ +
+ +
+ Ảnh icon +
+
+ +
+ Ảnh đại diện +
+
+
+ +
+ Thông tin danh mục +
+ + +
+

ví dụ: 300000;800000;1500000 có nghĩa là tạo ra 4 khoảng giá cho + khách hàng lọc Sản + phẩm, đó là:

+

- Dưới 300000,

+

- Từ 300000 đến 800000,

+

- Từ 800000 đến 1500000

+

- Trên 1500000

+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ Dùng cho SEO +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ Thay đổi Link truy cập khi thay đổi Tên index? + + Tích chọn để hiển thị hoặc dừng hiển + thị +
+
+ +
+ +
+ +
+
+
+ + +
+ +
\ No newline at end of file diff --git a/template/product/collection.html b/template/product/collection.html new file mode 100644 index 0000000..f0bdf7c --- /dev/null +++ b/template/product/collection.html @@ -0,0 +1,80 @@ +
+

Bộ sưu tập

+ +
+
+
+ + +
+
+
+ +
+ Hướng dẫn: Bộ sưu tập Sản phẩm bao gồm các Sản phẩm cùng chia sẻ một số đặc điểm nhất định, giúp cho + việc + quảng bá và + định hướng khách hàng hơn. Các bộ sưu tập thường được tạo là: Sản phẩm cho sinh nhật, Sản phẩm cho trẻ mới + sinh, Sản + phẩm công công nghệ mới nhất, Đồ gia dụng nên mua, Khuyến mại giảm giá tháng 10, Chúng tôi đề xuất với bạn, + Top 10 phụ + kiện nên có v..v. (mỗi bộ sưu tập sẽ bao gồm các Sản phẩm từ nhiều danh mục Sản phẩm khác nhau) +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + +
STTIDTên gọi/miêu tảSản phẩmLượt xemCập nhật
126 + Flash Sale | Giảm Giá Cực Sốc ! +
+

Link

+ +
+
+
+

7 sp

+ Xem + danh sách +
+
+ 638 + +
+ Xem + + + + +
+
+
+ +
\ No newline at end of file diff --git a/template/product/collection_add.html b/template/product/collection_add.html new file mode 100644 index 0000000..0dda57c --- /dev/null +++ b/template/product/collection_add.html @@ -0,0 +1,97 @@ +
+ +
+
+
+

Cập nhật bộ sưu tập Sản phẩm

+ +
+

Flash Sale | Giảm Giá Cực Sốc !

+
+ +
+
+ +
+
+
+ Thông tin cơ bản + +
+ Ảnh icon +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ Dùng cho SEO +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ +
+
\ No newline at end of file diff --git a/template/product/config_group.html b/template/product/config_group.html new file mode 100644 index 0000000..b310bad --- /dev/null +++ b/template/product/config_group.html @@ -0,0 +1,49 @@ +
+

Nhóm cấu hình sản phẩm

+ +
+
+ + Tìm kiếm +
+
+ + + +
+ + + + + + + + + + + + + + + +
STTThông tinCập nhật
1 + Máy In Canon Pixma IX6870-In Phun Mầu +

+ Tạo lúc: 28-12-2023, 11:34 am, + cập nhật: 28-12-2023, 11:34 am +

+
+
+ + +
+
+
+
\ No newline at end of file diff --git a/template/product/config_group_attribute.html b/template/product/config_group_attribute.html new file mode 100644 index 0000000..5226ba8 --- /dev/null +++ b/template/product/config_group_attribute.html @@ -0,0 +1,125 @@ +
+ + + +
+
+
+

Cập nhật cấu hình sản phẩm

+ +
+
+ +
+ +
+ +
+ Thuộc tính +
+
+ + +
+
+ + +
+
+
+ +
+ Thuộc tính + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + Thêm giá trị + +
+ +
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/template/product/config_group_form.html b/template/product/config_group_form.html new file mode 100644 index 0000000..f07cff2 --- /dev/null +++ b/template/product/config_group_form.html @@ -0,0 +1,66 @@ +
+ + + +
+
+
+

Cập nhật cấu hình sản phẩm

+ +
+
+ +
+ +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/template/product/config_group_product.html b/template/product/config_group_product.html new file mode 100644 index 0000000..df8ac36 --- /dev/null +++ b/template/product/config_group_product.html @@ -0,0 +1,88 @@ +
+ + + +
+
+
+

Cập nhật cấu hình sản phẩm

+ +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + +
STTSản phẩmThuộc tínhCông cụ
1 + Tai nghe Bluetooth True Wireless WIWU TWS12 Màu + Hồng + +
+

Màu sắc

+ +
+
+ +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/template/product/form.html b/template/product/form.html new file mode 100644 index 0000000..91cf8b1 --- /dev/null +++ b/template/product/form.html @@ -0,0 +1,109 @@ + + +
+ +
+
+
+

Cập nhật #{{ page.product_info.id }}

+
+ +
+ {{ page.product_info.title }} +
+ +
+
+ +
+
+ + {% if page.view_part == "store" %} + {% include 'product/form_components/store' %} + + {% elsif page.view_part == "category" %} + {% include 'product/form_components/category' %} + + {% elsif page.view_part == "seo" %} + {% include 'product/form_components/seo' %} + + {% elsif page.view_part == "description" %} + {% include 'product/form_components/description' %} + + {% elsif page.view_part == "spec-group" %} + {% include 'product/form_components/spec_group' %} + + {% elsif page.view_part == "instruction" %} + {% include 'product/form_components/instruction' %} + + {% elsif page.view_part == "image" %} + {% include 'product/form_components/image' %} + + {% elsif page.view_part == "variant" %} + {% include 'product/form_components/variant' %} + + {% elsif page.view_part == "accessory" %} + {% include 'product/form_components/accessory' %} + + {% elsif page.view_part == "addon" %} + {% include 'product/form_components/addon' %} + + {% elsif page.view_part == "video" %} + {% include 'product/form_components/video' %} + + {% elsif page.view_part == "tag" %} + {% include 'product/form_components/tag' %} + + {% elsif page.view_part == "relation" %} + {% include 'product/form_components/relation' %} + + {% elsif page.view_part == "customer-group" %} + {% include 'product/form_components/customer_group' %} + + {% elsif page.view_part == "component" %} + {% include 'product/form_components/component' %} + + {% elsif page.view_part == "configurable" %} + {% include 'product/form_components/configurable' %} + + {% elsif page.view_part == "compatible" %} + {% include 'product/form_components/compatible' %} + + {% elsif page.view_part == "similar" %} + {% include 'product/form_components/similar' %} + + {% elsif page.view_part == "combo-set" %} + {% include 'product/form_components/combo_set' %} + + {% else %} + + {% include 'product/form_components/basic' %} + {% endif %} + +
+
+
\ No newline at end of file diff --git a/template/product/form_components/accessory.html b/template/product/form_components/accessory.html new file mode 100644 index 0000000..97524b8 --- /dev/null +++ b/template/product/form_components/accessory.html @@ -0,0 +1,123 @@ + +
+ + +
+ Các danh mục + + +
+ PC, Workstation +
+ + + + + + + + + + + + + + + + + + + +
STTẢnhSản phẩmGiá hiện tạiQuản lý
1 + + + CPU Intel Xeon E5-2665 2.40 GHz / 20MB / 8 Cores 16 Threads / + Socket 2011 +
+ SKU: HN123567 + / + Số lượng: 1 +
+
+ 24.900.000 VND + + + + +
+
+
+
+ PC, Workstation +
+ + + + + + + + + + + + + + + + + + + +
STTẢnhSản phẩmGiá hiện tạiQuản lý
1 + + + CPU Intel Xeon E5-2665 2.40 GHz / 20MB / 8 Cores 16 Threads / + Socket 2011 +
+ SKU: HN123567 + / + Số lượng: 1 +
+
+ 24.900.000 VND + + + + +
+
+
+
+ +
\ No newline at end of file diff --git a/template/product/form_components/addon.html b/template/product/form_components/addon.html new file mode 100644 index 0000000..e69de29 diff --git a/template/product/form_components/basic.html b/template/product/form_components/basic.html new file mode 100644 index 0000000..3a5d8ee --- /dev/null +++ b/template/product/form_components/basic.html @@ -0,0 +1,304 @@ + +
+
+ Thông tin cơ bản +
+ +
+ +
+ Bạn muốn Google tìm ra Sản phẩm này hoặc người xem hiểu ngay về Sản phẩm ? Hãy viết tên Sản phẩm + một cách cụ thể, đầy đủ + nhất. Thay vì viết Vaio ABC, hãy viết : Máy tính xách tay Sony Vaio ABC, 500GB, 4GB RAM, 2.4GHz, + USA, mới 100% Hoặc thay + vì viết Áo 123, hãy viết : Áo khoác nam Made In VietNam, màu đen, đủ size, mã 123 +
+
+
+ + +
+
+
+ +
+ +
+ Nhiều khách hàng tìm Sản phẩm theo Model, bạn có nhập model của Sản phẩm nếu có. Ví dụ: + Laptop Sony Vaio ABC/12 thì + ABC/12 là model Sản phẩm. +
+
+
+ +
+
+
+ +
+ +
+ Nếu bạn có phần mềm quản lý kho hàng chuyên nghiệp và bạn muốn theo dõi các đơn hàng nhận từ + website của Sản phẩm trong + phần mềm này. Khi đó hãy nhập mã kho hàng của Sản phẩm được cung cấp bởi phần mềm kho hàng + vào đây. +
+
+
+ +
+
+
+ + gram +
+ +
+ Dùng tính phí Shipping cho Sản phẩm (nếu có). Đơn vị gram, nếu Sản phẩm 1,2kg = 1200 (gram). +
+
+
+ + +
+
+
+ + +
+ +
+ +
+ +
+ + +
+
+ + +
+
+
+ Thông tin bán hàng + +
+
+ +
+ (* Nếu dùng USD thì theo chuẩn quốc tế v.d. $134.05). + Tỷ giá: + 1 + + + +
+
+
+ + + / + +
+ +
+ +
+
+
+ +
+ vnd (để tham khảo khi cần) +
+
+ + +
+
+
+ +
+ vnd (để tham khảo khi cần) +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ +
+ + + + + + + +
+
+ +
+ +
+ + +
+
+
+
+ +
+
\ No newline at end of file diff --git a/template/product/form_components/category.html b/template/product/form_components/category.html new file mode 100644 index 0000000..eb18f4a --- /dev/null +++ b/template/product/form_components/category.html @@ -0,0 +1,43 @@ + +
+ Hiện tại Sản phẩm đang được đặt trong các danh mục sau : + + +

+ Bạn có thể chọn thêm danh mục khác cho Sản phẩm hoặc chỉnh lại. +

+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
\ No newline at end of file diff --git a/template/product/form_components/combo_set.html b/template/product/form_components/combo_set.html new file mode 100644 index 0000000..e69de29 diff --git a/template/product/form_components/combo_set_select.html b/template/product/form_components/combo_set_select.html new file mode 100644 index 0000000..e69de29 diff --git a/template/product/form_components/compatible.html b/template/product/form_components/compatible.html new file mode 100644 index 0000000..e69de29 diff --git a/template/product/form_components/component.html b/template/product/form_components/component.html new file mode 100644 index 0000000..e69de29 diff --git a/template/product/form_components/configurable.html b/template/product/form_components/configurable.html new file mode 100644 index 0000000..e69de29 diff --git a/template/product/form_components/customer_group.html b/template/product/form_components/customer_group.html new file mode 100644 index 0000000..e69de29 diff --git a/template/product/form_components/description.html b/template/product/form_components/description.html new file mode 100644 index 0000000..7ca5925 --- /dev/null +++ b/template/product/form_components/description.html @@ -0,0 +1,27 @@ + +
+ +
+ Chưa có thư viện ảnh cho phần miêu tả +
+ + + + +
\ No newline at end of file diff --git a/template/product/form_components/image.html b/template/product/form_components/image.html new file mode 100644 index 0000000..c2e2b2a --- /dev/null +++ b/template/product/form_components/image.html @@ -0,0 +1,187 @@ + +
+
+ Cập nhật ảnh sản phẩm + +
+
+
+ +
+
+
+
+ Hình sản phẩm +

(Ảnh chính)

+
+
+
+

STT

+ +
+
+

ALT

+ +
+
+
+ +
+
+
+
+ +
+
+
+
+ Hình sản phẩm +

(Ảnh chính)

+
+
+
+

STT

+ +
+
+

ALT

+ +
+
+
+ +
+
+
+
+ +
+
+
+
+ Hình sản phẩm +

(Ảnh chính)

+
+
+
+

STT

+ +
+
+

ALT

+ +
+
+
+ +
+
+
+
+ +
+
+
+
+ Hình sản phẩm +

(Ảnh chính)

+
+
+
+

STT

+ +
+
+

ALT

+ +
+
+
+ +
+
+
+
+ +
+
+
+
+ Hình sản phẩm +

(Ảnh chính)

+
+
+
+

STT

+ +
+
+

ALT

+ +
+
+
+ +
+
+
+
+ +
+ Thêm ảnh cho sản phẩm +

Bạn có thể: Chọn ảnh có sẵn trong kho ảnh, hoặc upload ảnh từ máy tính:

+ +
+ + +

Chú ý: Bạn có thể chọn nhiều ảnh cùng 1 + lúc: + Chấp nhận các file ảnh: jpg,gif

+ +
+ +
+ Chú ý: +
    +
  • - Ảnh Sản phẩm kích thước lớn sẽ được tự động co lại thành các ảnh + nhỏ hơn cho những vị trí liên quan. Tùy giao + diện website của bạn mà kích thước ảnh Sản phẩm khác nhau. Click + vào đây để + quy định kích thước ảnh Sản phẩm.
  • +
  • + - Chỉ dùng file ảnh đuôi .jpg và .gif. +
  • +
  • + - Cập nhật hình ảnh cho Sản phẩm ở nhiều góc cạnh, màu sắc để người dùng xem + rõ nhất. Nên cập nhật ảnh có kích thước lớn + và độ phân giải cao. +
  • +
+
+
\ No newline at end of file diff --git a/template/product/form_components/instruction.html b/template/product/form_components/instruction.html new file mode 100644 index 0000000..e69de29 diff --git a/template/product/form_components/price.html b/template/product/form_components/price.html new file mode 100644 index 0000000..e69de29 diff --git a/template/product/form_components/relation.html b/template/product/form_components/relation.html new file mode 100644 index 0000000..42f0c4c --- /dev/null +++ b/template/product/form_components/relation.html @@ -0,0 +1,12 @@ +
+
+ Danh sách liên quan + + + + +
+
\ No newline at end of file diff --git a/template/product/form_components/seo.html b/template/product/form_components/seo.html new file mode 100644 index 0000000..eebe8ac --- /dev/null +++ b/template/product/form_components/seo.html @@ -0,0 +1,76 @@ + +
+
+
+ + +
+
+ + +
+ +
+ +
+ +
+
+
+
+ + * để trống sẽ dùng link mặc định của hệ thống +
+ +
+ +
+
+
+ + * 0 ký tự , 0 từ +
+ +
+
+
+ + * 0 ký tự , 0 từ +
+ +
+
+ +
+
+ + * 0 ký tự , 0 từ (Khuyến nghị: 160 ký tự) +
+ +
+ +
+ +
+ +
+
\ No newline at end of file diff --git a/template/product/form_components/similar.html b/template/product/form_components/similar.html new file mode 100644 index 0000000..e69de29 diff --git a/template/product/form_components/spec.html b/template/product/form_components/spec.html new file mode 100644 index 0000000..e69de29 diff --git a/template/product/form_components/spec_group.html b/template/product/form_components/spec_group.html new file mode 100644 index 0000000..aaf2db9 --- /dev/null +++ b/template/product/form_components/spec_group.html @@ -0,0 +1,151 @@ + +
+ +
+ Cập nhật thuộc tính cho Sản phẩm + +
+ +
+ +
+ Dòng cpu +

dong-cpu

+ Dùng là bộ lọc - Dùng tạo lựa chọn SP - Hiển thị ở tóm tắt +
+ + + + + +
+
+
+ Bổ sung giá trị: +

mỗi giá trị 1 dòng

+ +
+ +
+
+ +
+ Dòng cpu +

dong-cpu

+ Dùng là bộ lọc - Dùng tạo lựa chọn SP - Hiển thị ở tóm tắt +
+ + + + + +
+
+
+ Bổ sung giá trị: +

mỗi giá trị 1 dòng

+ +
+ +
+
+
+ Dòng cpu +

dong-cpu

+ Dùng là bộ lọc - Dùng tạo lựa chọn SP - Hiển thị ở tóm tắt +
+ + + + + +
+
+
+ Bổ sung giá trị: +

mỗi giá trị 1 dòng

+ +
+ +
+
+ +
+ + +
\ No newline at end of file diff --git a/template/product/form_components/spec_multiple.html b/template/product/form_components/spec_multiple.html new file mode 100644 index 0000000..e69de29 diff --git a/template/product/form_components/store.html b/template/product/form_components/store.html new file mode 100644 index 0000000..e69de29 diff --git a/template/product/form_components/tag.html b/template/product/form_components/tag.html new file mode 100644 index 0000000..ba5efc1 --- /dev/null +++ b/template/product/form_components/tag.html @@ -0,0 +1,39 @@ +
+
+ + +
+ + + + + + + + + + + +
STTTag
1ATO Test + + + +
+
+ +
+
+ Thêm tag nhanh cho sản phẩm +

Nhập Tag cho sản phẩm vào ô dưới đây và nhấn cập nhật. Mỗi Tag 1 dòng:

+ + + +
+
\ No newline at end of file diff --git a/template/product/form_components/variant.html b/template/product/form_components/variant.html new file mode 100644 index 0000000..8acb86d --- /dev/null +++ b/template/product/form_components/variant.html @@ -0,0 +1,139 @@ + +
+
+
+ Chủng loại của sản phẩm + + + + Sửa thuộc tính + + +
+
+ Chú ý: + Giá bán của sản phẩm sẽ là giá của chủng loại được chọn. Nếu giá chủng loại=0 thì sẽ mặc định dùng + giá sản phẩm. + Cài đặt giá chung cho sản phẩm ở Tab "Cơ bản" + +
+ +
+ Cài nhanh cho tất cả chủng loại + +
+
+ + +
+
+ + +
+
+
+ +
+ + + + + + + + + + + + + + + +
ẢnhChủng loại
+ + + + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+ + + + Thêm chủng loại + +
+
+ + +
+
\ No newline at end of file diff --git a/template/product/form_components/video.html b/template/product/form_components/video.html new file mode 100644 index 0000000..b98b3a5 --- /dev/null +++ b/template/product/form_components/video.html @@ -0,0 +1,42 @@ +
+
+ Youtobe Video +

Bạn cần gắn url của video được cung cấp bởi Youtube vào đây +

+ Ví dụ: https://www.youtube.com/watch?NR=1&v=QqzJhk1p4rU&feature=endscreen +
+ + + + + + + + + + + +
Link youtobeMô tả (nếu có)
+ + + + + + + +
+
+ + + + + Thêm chủng loại + +
+
+ + +
+
\ No newline at end of file diff --git a/template/product/home.html b/template/product/home.html new file mode 100644 index 0000000..ba0549c --- /dev/null +++ b/template/product/home.html @@ -0,0 +1,324 @@ +
+

Danh sách sản phẩm ({{ page.total | format_number }})

+
+
+
+ +
+ + Chọn thương hiệu để xem + +
+ +
+
+ + + + + + + + + + + + + {% assign counter = 0 %} + {% for item in page.item_list %} + {% increment counter %} + + + + + + + + + {% endfor %} + + +
STTẢnhSản phẩm (Tổng số: {{page.total}})Thông tin bán hàng + + Công cụ
{{ counter }} + + {{item.image_count}} ảnh + + [#{{item.id}}] + - + {{item.title}} + + {% if item.config_count > 0 %}

[Có cấu hình]

{% endif %} +
+ Danh mục: + Màn hình Máy tính +
+
+ Mã kho + {{item.sku}} + | + Hãng + +
+ + {% assign date_now = "now" | date: format %} + {% assign date_last = item.last_update %} + {% assign time_conlai = date_now | minus: date_last %} + +

Cập nhật: {{item.last_update | date: "Y-m-d h:i" }} ( + {{ time_conlai | divided_by: 86400 | round}} + ngày)

+

Người cập nhật : {{item.last_update_by}}

+
+
+ Giá bán: + + {% if item.price > 0 %} + {{item.price | format_price }} vnd + {% else %}` + {{item.price }} vnd + {% endif %} + +
+
+ Giá thị trường: + + {% if item.market_price > 0 %} {{item.market_price | format_price }} vnd {% else %} + 0 vnd {% endif %} + +
+

Bảo hành: {{item.warranty}}

+

Khuyến mại: {{item.special_offer}}

+
+ SL tổng: + {{item.quantity}} +
+
+ + + + + + + + + + + + +
+
+
+ + + + + + + {% if item.status == 0 %} + + + + {% else %} + + + + {% endif %} + + + +
+
+
+
+ + + + 1 + 2 + 3 + 4 + 5 + + + +
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/template/product/list_competitor.html b/template/product/list_competitor.html new file mode 100644 index 0000000..01e3650 --- /dev/null +++ b/template/product/list_competitor.html @@ -0,0 +1,130 @@ +
+

So sánh giá đối thủ

+ +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTSản phẩm (Tổng số: 53)Cập nhật
1 + Ổ Cứng SSD KINGMAX 120GB SMV32 +

- Giá: 499.000

+

-Mã kho: OCKM0008

+

- Cập nhật: 25-01-2024, 9:46 am (29 ngày)

+
+ Cập + nhật +
1 + Ổ Cứng SSD KINGMAX 120GB SMV32 +

- Giá: 499.000

+

-Mã kho: OCKM0008

+

- Cập nhật: 25-01-2024, 9:46 am (29 ngày)

+
+ Cập + nhật +
1 + Ổ Cứng SSD KINGMAX 120GB SMV32 +

- Giá: 499.000

+

-Mã kho: OCKM0008

+

- Cập nhật: 25-01-2024, 9:46 am (29 ngày)

+
+ Cập + nhật +
1 + Ổ Cứng SSD KINGMAX 120GB SMV32 +

- Giá: 499.000

+

-Mã kho: OCKM0008

+

- Cập nhật: 25-01-2024, 9:46 am (29 ngày)

+
+ Cập + nhật +
+
+
+ + + + + + \ No newline at end of file diff --git a/template/product/set.html b/template/product/set.html new file mode 100644 index 0000000..a3de534 --- /dev/null +++ b/template/product/set.html @@ -0,0 +1,64 @@ +
+

Danh sách set

+
+
Giới thiệu: "Combo Set" là một "set" (danh sách) các sản phẩm được cài giảm giá + sẵn. + Các + mức giá giảm này + sẽ được áp + dụng khi người mua hàng mua cùng 1 sản phẩm chính. Ví dụ đơn giản: Bạn tạo 1 set (danh sách) các phụ + kiện dành cho + iPhone với các mức chiết khấu đáng kể nếu người dùng mua kèm iPhone. Sau khi tạo set xong, bạn chọn 1 + vài sản phẩm + iPhone có thể áp dựng set này. Như vậy xem website có thể chọn mua iPhone và mua thêm 1 số sản phẩm + trong set mà bạn đã + thiết lập.
+ + + Thêm + set mới +
+
+ + + + + + + + + + + + + + + + + + + +
STTThông tinCập nhậtSản phẩm sử dụng set nàyQuản lý
1(ID: 658) Thẻ Nhớ CAEZ CAIM CAIS +

Thẻ Nhớ cho các dòng CAM: CAEZ CAIM CAIS

+
+

Nguyễn Tiến Dinh

+

01-03-2024, 11:59 am

+
+
+

137 sp

+ Xem + danh sách +
+
+
+ Chi + tiết + + +
+
+
+ +
\ No newline at end of file diff --git a/template/product/set_form.html b/template/product/set_form.html new file mode 100644 index 0000000..42c049e --- /dev/null +++ b/template/product/set_form.html @@ -0,0 +1,320 @@ +
+ +
+
+
+

Cập nhật combo set

+ + + + Thêm set mới + +
+

Thẻ Nhớ CAEZ CAIM CAIS

+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
Tên set + +
+ Mô tả tóm tắt + + +
+ Trạng thái + + + + +
+ Thời trang hiển thị + +
+

Bắt đầu

+
+ + +
+

Giờ

+ +
+ +
+

Kết thúc

+
+ + +
+

Giờ

+ +
+
+ Nhóm sản phẩm trong Set + + + + + + + + + + +
+ Nhóm sản phẩm + + +

Tên gọi

+ +
+ Sản phẩm + + +
+

Tên gọi

+ [Chọn sản + phẩm] +
+ +

Giá đang bán : 129.000

+
+

Cài giảm

+ + + +
+
+ + + +
+
+ [+] Thêm + nhóm sản phẩm +
+ + + +
+
+
+
+
\ No newline at end of file diff --git a/template/product/set_product.html b/template/product/set_product.html new file mode 100644 index 0000000..ea83f58 --- /dev/null +++ b/template/product/set_product.html @@ -0,0 +1,308 @@ +
+ +
+
+

Cập nhật combo set

+

Thẻ Nhớ CAEZ CAIM CAIS

+
+
+
+ +
+
+
+
+

Danh sách sản phẩm sử dụng set

+ + + + Thêm set mới + +
+ +
+
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTẢnhTênLựa chọn
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
1 + + +

(#71342)

+ CAMERA AI WIFI XOAY TRÁI/ XOAY PHẢI ISACHI SC-B1 +

Mã kho: CAIS013

+

Kho hàng: 8

+

Giá bán : 749.000 vnd

+
+ +
+ + + +
+
+
+
+
\ No newline at end of file diff --git a/template/product/spec_group.html b/template/product/spec_group.html new file mode 100644 index 0000000..4d81955 --- /dev/null +++ b/template/product/spec_group.html @@ -0,0 +1,49 @@ +
+

Nhóm thông số kỹ thuật

+ +
+ +
+ +
+

+ Hướng dẫn: + Nhóm thông số kỹ thuật được ghép cho từng sản phẩm cụ thể +

+ + + Thêm nhóm mới + +
+
+ + + + + + + + + + + + + + + +
STTTên gọi/miêu tảCập nhật
1 + Camera + +
+ + +
+
+
+
\ No newline at end of file diff --git a/template/product/spec_group_add.html b/template/product/spec_group_add.html new file mode 100644 index 0000000..dbedfb9 --- /dev/null +++ b/template/product/spec_group_add.html @@ -0,0 +1,77 @@ +
+ + + +
+
+
+

Cập nhật thông số kỹ thuật

+ Thông tin chung +
+
+
+
+ +
+
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/template/product/spec_group_info.html b/template/product/spec_group_info.html new file mode 100644 index 0000000..d43e362 --- /dev/null +++ b/template/product/spec_group_info.html @@ -0,0 +1,95 @@ +
+ + + +
+

Thông tin nhóm thông số kỹ thuật

+
+
+ Nhóm thông số +

THIẾT BỊ TRỢ GIẢNG

+
+ + + + Thêm nhóm mới + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTHeaderThông tinDiễn dảiThứ tự hiển thịBắt buộc nhập cho spCập nhật
1Thông tin chungĐặc tính-99 +
+ + +
+
1Thông tin chungĐặc tính-99 +
+ + +
+
1Thông tin chungĐặc tính-99 +
+ + +
+
+
+
\ No newline at end of file diff --git a/template/report/customer.html b/template/report/customer.html new file mode 100644 index 0000000..412339a --- /dev/null +++ b/template/report/customer.html @@ -0,0 +1,122 @@ +
+
+

Thống kê truy cập

+ +
+
+
+
+ 1.45K + Truy cập web trong tuần này +
+
+
+ 12% + +
+
+
+ +
+ +
+ +
+ +
+
+
+ Truy cập website + +
+ +
+ +
+ +
+ +
+
+
+ Truy cập theo tỉnh thành + +
+ +
+ +
+ +
+
+
+ + +
+
+
+
+

Khách hàng đăng ký mới trong 30 ngày:

+ 11.458 +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTNgàySố truy cập
105-12-2021235
105-12-2021235
105-12-2021235
105-12-2021235
105-12-2021235
+ +
+
+ +
+
\ No newline at end of file diff --git a/template/report/order.html b/template/report/order.html new file mode 100644 index 0000000..afcb472 --- /dev/null +++ b/template/report/order.html @@ -0,0 +1,131 @@ +
+
+

Thống kê đơn hàng

+ +
+
+
+
+ 1.45K + Truy cập web trong tuần này +
+
+
+ 12% + +
+
+
+ +
+ +
+ +
+ +
+
+
+ Truy cập website + +
+ +
+ +
+ +
+ +
+
+
+ Truy cập theo tỉnh thành + +
+ +
+ +
+ +
+
+
+ + +
+
+
+
+

Khách hàng đăng ký mới trong 30 ngày:

+ 11.458 +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTNgàySố đơn hàng mớiTình trạng
105-12-2021235 + +
105-12-2021235 + +
105-12-2021235 + +
105-12-2021235 + +
+ +
+
+ +
+
\ No newline at end of file diff --git a/template/report/product_buy.html b/template/report/product_buy.html new file mode 100644 index 0000000..88b75d4 --- /dev/null +++ b/template/report/product_buy.html @@ -0,0 +1,101 @@ +
+
+

Sản phẩm mua nhiều

+ +
+
+ Top sản phẩm mua nhiều + +
+
+ + + +
+
+ +
+
+
+

Khách hàng đăng ký mới trong 30 ngày:

+ 11.458 +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTMã sốSản phẩmSố đơn
1#2081 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 223
1#2081 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 223
1#2081 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 223
1#2081 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 223
+
+ + +
+
\ No newline at end of file diff --git a/template/report/product_visit.html b/template/report/product_visit.html new file mode 100644 index 0000000..4017c52 --- /dev/null +++ b/template/report/product_visit.html @@ -0,0 +1,101 @@ +
+
+

Sản phẩm xem nhiều

+ +
+
+ Top sản phẩm xem nhiều + +
+
+ + + +
+
+ +
+
+
+

Khách hàng đăng ký mới trong 30 ngày:

+ 11.458 +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTMã sốSản phẩmSố đơn
1#2081 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 223
1#2081 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 223
1#2081 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 223
1#2081 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 223
+
+ + +
+
\ No newline at end of file diff --git a/template/report/referer.html b/template/report/referer.html new file mode 100644 index 0000000..156bdb2 --- /dev/null +++ b/template/report/referer.html @@ -0,0 +1,113 @@ +
+
+

Website giới thiệu

+ +
+
+
+
+ 1.45K + Truy cập web trong tuần này +
+
+
+ 12% + +
+
+
+ +
+ +
+ +
+ +
+
+
+ Khách hàng biết tới website + +
+ +
+ +
+ +
+ +
+
+ +
+
+
+

Các website mang người xem đến website của quý khách nhiều nhất trong tháng

+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STTNguồnLượt giới thiệu
1 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 278
1 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 278
1 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 278
+
+ +
+ Giải thích thuật ngữ: +
    +
  • Khi quý khách quảng cáo Sản phẩm, tin tức ở diễn đàn hay ở website nào khác, + hãy cố gắng đặt đường link để + người dùng click vào link đó và truy cập vào website của quý khách. Càng nhiều đường link, website + của + quý khách sẽ được + các bộ máy tìm kiếm như Google, Yahoo, Bing tìm đến và lưu lại thông tin. Như vậy sẽ có hiệu quả tốt + khi + người dùng tìm + kiếm Google và nhìn thấy được website của quý khách mà không phải là tin bài quý khách đã quảng cáo + ở + các website khác. +
  • +
+
+ +
+
\ No newline at end of file diff --git a/template/report/search.html b/template/report/search.html new file mode 100644 index 0000000..126025f --- /dev/null +++ b/template/report/search.html @@ -0,0 +1,95 @@ +
+
+

Thống kê tìm kiếm

+ +
+
+
+
+ 1.45K + Truy cập web trong tuần này +
+
+
+ 12% + +
+
+
+ +
+ +
+ +
+ +
+
+
+ Công cụ tìm kiếm website + +
+ +
+ +
+ +
+ +
+
+ +
+
+
+

Tìm kiếm tại website nhiều nhất trong 30 ngày qua

+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
STTNguồnLượt giới thiệu
1 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 278
1 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 278
1 + Tai nghe NICEHCK Traceless có Mic (Type-C) + 278
+
+ +
+
\ No newline at end of file diff --git a/template/report/visitor.html b/template/report/visitor.html new file mode 100644 index 0000000..1ac4fb9 --- /dev/null +++ b/template/report/visitor.html @@ -0,0 +1,198 @@ +
+
+

Thống kê truy cập

+ +
+
+
+
+ 1.45K + Truy cập web trong tuần này +
+
+
+ 12% + +
+
+
+ +
+ +
+ +
+ +
+
+
+ Truy cập website + +
+ +
+ +
+ +
+ +
+
+
+ Truy cập theo tỉnh thành + +
+ +
+ +
+ +
+
+
+ + +
+
+
+
+

Tổng số lượt truy cập trong 30 ngày:

+ 11.458 +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTNgàySố truy cập
105-12-2021235
105-12-2021235
105-12-2021235
105-12-2021235
105-12-2021235
+ +
+
+
+
+

Tổng số lượt truy cập trong 30 ngày:

+ 11.458 +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTNgàySố truy cập
105-12-2021235
105-12-2021235
105-12-2021235
105-12-2021235
105-12-2021235
+ +
+
+ +
+ Giải thích thuật ngữ: +
    +
  • Giới thiệu bởi: + Giới thiệu bởi : Là địa chỉ website mang người xem tới website. Ví dụ: Người xem đang xem website + abcd.com và thấy có + đường link Sản phẩm, người đó click vào đường link để tới website của bạn, như vậy website abcd.com + sẽ được hệ thống lưu + lại. Hãy quảng cáo link Sản phẩm hoặc gian hàng của bạn ở các website khác để tăng sự giới thiệu. +
  • +
  • + IP : Đây là địa chỉ IP của máy tính của người xem. Mỗi máy tính đều có 1 địa chỉ riêng để + nhận + biết (và gọi IP). Trong + một mạng nội bộ chia sẻ (ví dụ: quán Internet Cafe) thì nhiều máy dùng chung 1 đường dây mạng sẽ + có cùng 1 địa chỉ IP + (khi truy cập bên ngoài). +
  • +
  • + Thời gian : Thời gian truy cập website +
  • +
  • + Trang được xem : Trang chi tiết trên website được xem. +
  • +
+
+ +
+
\ No newline at end of file diff --git a/template/shipping2/fee.html b/template/shipping2/fee.html new file mode 100644 index 0000000..6afd946 --- /dev/null +++ b/template/shipping2/fee.html @@ -0,0 +1,50 @@ +
+ + + +
+ Chọn danh sách tỉnh thành để quản lý phí tới Quận/Huyện + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTTênQuản lý
1Hà nộiQuản lý +
1Hà nộiQuản lý +
1Hà nộiQuản lý +
1Hà nội + Quản lý +
+ +
+ +
\ No newline at end of file diff --git a/template/shipping2/home.html b/template/shipping2/home.html new file mode 100644 index 0000000..412ab38 --- /dev/null +++ b/template/shipping2/home.html @@ -0,0 +1,22 @@ +
+
+

Quản lý shipping

+ +
+

Cài đặt hệ thống chung

+ + + Cài đặt + +
+
+

Cài đặt phí ship từng vùng

+ + + Cài đặt + +
+
+
\ No newline at end of file diff --git a/template/shipping2/setting.html b/template/shipping2/setting.html new file mode 100644 index 0000000..9480077 --- /dev/null +++ b/template/shipping2/setting.html @@ -0,0 +1,91 @@ +
+ + + +
+
+ Cài đặt giá trị đơn hàng: +
+

Giá trị đơn hàng không tính giá : +

+ +
+
(* Đơn hàng có giá trị nhỏ hơn giá trị này sẽ được áp dụng biểu + phí theo + vùng)
+ + Cài đặt phí thu hộ: + + + + + + + + + + + + + + + + + + + + + + +
STTGiá trị đơn hàng dưới mứcPhú thu hộ (VNĐ)Quản lý
1 + + + + + +
1 + + + + + +
+ + [+] Thêm hàng + + + +
+ Hướng dẫn cài phí thu hộ +

Phí thu hộ được tính theo giá trị cần thu hộ, ví dụ:

+
    +
  • - Từ 0 đến <=1 triệu=30.000đ=> Cần nhập là 1.000.000
  • +
  • - Từ >1 triệu đến <= 2 triệu=50.000đ=> Cần nhập là 2.000.000
  • +
  • - Từ >2 triệu đến <= 3 triệu=70.000đ=> Cần nhập là 3.000.000
  • +
  • - Từ >3 triệu đến <= 4 triệu=90.000đ=> Cần nhập là 4.000.000
  • +
  • - Từ >4 triệu đến <= 5 triệu=110.000đ=> Cần nhập là 5.000.000
  • +
  • - Từ >5 triệu đến <= 6 triệu=130.000đ=> Cần nhập là 6.000.000
  • +
  • - Từ >6 triệu đến <= 7 triệu=150.000đ=> Cần nhập là 7.000.000
  • +
  • - Từ >7 triệu đến <= 8 triệu=170.000đ=> Cần nhập là 8.000.000
  • +
  • - Từ >8 triệu đến <= 9 triệu=190.000đ=> Cần nhập là 9.000.000
  • +
  • - Trên 9 triệu = 0đ => Cần nhập là 10.000.000.000 (nhập số lớn nhất có thể)
  • +
+

Để xóa phí nào nó, click "Xóa bỏ" và nhấn "Cập nhật" để lưu lại

+
+ +
+
+
\ No newline at end of file diff --git a/template/system/ban_ip.html b/template/system/ban_ip.html new file mode 100644 index 0000000..119be5f --- /dev/null +++ b/template/system/ban_ip.html @@ -0,0 +1,72 @@ + + +
+
+

Chặn IP xấu truy cập website

+

Chức năng này cho phép bạn ngăn chặn một số địa chỉ IP truy cập vào + website. Thường dùng + để chặn đối thủ hoặc + những + người có mục đích xấu muốn phá hoại website của bạn

+
+ + + +
+
+ Nhập IP cần cấm (mỗi IP 1 dòng) +
+ Bạn có thể xem địa chỉ IP tại Báo cáo + truy + cập. Để loại bỏ + IP khỏi danh sách cấm, xóa IP + trong danh sách + và cập nhật + lại. +
+ +
+ +
+

Hỗ trợ các định dạng: 127.0.0.1

+

172.0.0.*

+

173.0.*.*

+

126.1.0.0/24

+

125.0.0.1-125.0.0.9

+
+ +
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/template/system/change_info.html b/template/system/change_info.html new file mode 100644 index 0000000..fe104cc --- /dev/null +++ b/template/system/change_info.html @@ -0,0 +1,112 @@ + + +
+
+

Thay đổi nội dung doanh nghiệp

+
+ + + +
+
+
+
+ + * nếu để trống sẽ dùng tên gọi website +
+ +
+
+ + +
+
+ + +
+
+
+ +
+ +
+ + 1 + 2 + 3 + 4 + +
+
+
+ Thông tin giới thiệu chi tiết + +
+
+ Thông tin liên hệ + +
+
+
+ +
+
+
\ No newline at end of file diff --git a/template/system/email.html b/template/system/email.html new file mode 100644 index 0000000..8484aed --- /dev/null +++ b/template/system/email.html @@ -0,0 +1,394 @@ + + +
+
+

Cài đặt Email

+
+ + + +
+
+ Cài đặt email cho website + Cài đặt email + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTPhân loạiCài đặtHành động
1 + Email gửi thông báo cho khách hàng (*) +

Dùng gửi khách hàng khi họ đặt hàng, báo tình trạng đơn hàng v.v. Cần phải dùng một dịch + vụ gửi email để gửi đi. Mặc + định là dùng hotro@demopc8.hurasoft.com và sử dụng dịch vụ gửi email của HuraSoft. +

+
+
+

Email *

+ +
+
+ Cập + nhật +
2 + Email nhận chung (*) +

Bắt buộc. Sẽ được dùng để nhận tất cả các thông báo của website nếu các email khác dưới + đây không được cài. +

+
+
+

Email *

+ +
+
+ Cập + nhật +
3 + Email nhận thông báo đơn hàng +

Các đơn hàng mới sẽ được gửi thông báo vào Email này

+
+
+

Email *

+ +
+
+ Cập + nhật +
4 + Email nhận liên hệ +

Các liên hệ mới sẽ được gửi thông báo vào Email này

+
+
+

Email *

+ +
+
+ Cập + nhật +
+
+
+ Cài đặt email cho website +
+ Hướng dẫn: Nên dùng Editor ở phần sửa template để chỉnh + nội dung html rồi copy/cập nhật ở + đây +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTPhân loạiNội dungHành động
1 + Email gửi thông báo đơn hàng mới cho khách hàng
email_tpl_order_detail +
+ Xem + nội + dung + + + Cập + nhật +
2 + Email gửi thông báo cho khách hàng khi đơn hàng được cập nhật trạng thái + mới
email_tpl_update_buyer_order_status +
+ Xem + nội + dung + + + Cập + nhật +
3 + Email gửi thông báo đơn hàng mới cho khách hàng
email_tpl_order_detail +
+ Xem + nội + dung + + + Cập + nhật +
4 + Email gửi thông báo đơn hàng mới cho khách hàng
email_tpl_order_detail +
+ Xem + nội + dung + + + Cập + nhật +
5 + Email gửi thông báo đơn hàng mới cho khách hàng
email_tpl_order_detail +
+ Xem + nội + dung + + + Cập + nhật +
6 + Email gửi thông báo đơn hàng mới cho khách hàng
email_tpl_order_detail +
+ Xem + nội + dung + + + Cập + nhật +
7 + Email gửi thông báo đơn hàng mới cho khách hàng
email_tpl_order_detail +
+ Xem + nội + dung + + + Cập + nhật +
8 + Email gửi thông báo đơn hàng mới cho khách hàng
email_tpl_order_detail +
+ Xem + nội + dung + + + Cập + nhật +
9 + Email gửi thông báo đơn hàng mới cho khách hàng
email_tpl_order_detail +
+ Xem + nội + dung + + + Cập + nhật +
+ +
+
+
\ No newline at end of file diff --git a/template/system/form_setup/general.html b/template/system/form_setup/general.html new file mode 100644 index 0000000..0c4c19e --- /dev/null +++ b/template/system/form_setup/general.html @@ -0,0 +1,98 @@ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Tạm đóng website +

(Nếu bạn chưa muốn người dùng xem website, hãy tạm đóng lại)

+
+
+ Mật khẩu để xem + + (Xóa bỏ mật khẩu để mở lại website) +
+
+ Nội dung thông báo khách hàng: + +
+
+ Xác thực tên miền với Google Sử dụng khi cài email của Google + + +

Nhập nội dung của file Google vào ô trên

+
+ (

V.d.

google-site-verification: + google0b0cdca26a03ce2e.html) +
+
Tỷ giá ngoại tệ (USD/VND) + +
+ Số lượng sản phẩm / 1 trang danh mục + + +
+ Thứ tự sản phẩm mặc định trong danh mục + + +
+ Số lượng tin tức hiển thị / 1 trang + + +
+ SiteManager (nếu có) + + + v.d. http://web.hurasoft.com +
+ SiteManager Access Key + + + xem trong sitemanager +
+ + +
+
\ No newline at end of file diff --git a/template/system/form_setup/homepage.html b/template/system/form_setup/homepage.html new file mode 100644 index 0000000..b92d1e4 --- /dev/null +++ b/template/system/form_setup/homepage.html @@ -0,0 +1,121 @@ +
+
+ Hướng dẫn: +
    +
  • - ID danh mục: nhập danh sách các ID danh mục cách nhau dấu , hoặc khoảng trắng. + Để percat nếu muốn hệ thống + tự động lấy theo danh mục sản phẩm đang xem
  • +
  • - Số lượng hiển thị : Nhập 0 nếu không muốn dùng nội dung, điền số > 0 để có + số lượng cần lấy Để website chạy nhanh nhất, chỉ kích hoạt các nội dung mà giao diện hiển thị cần đến. +
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Sản phẩm nổi bật (HOT) + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Sản phẩm bán chạy nhất (BEST SALE) + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Sản phẩm mới nhất (NEW) + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Sản phẩm xả hàng (SALE OFF) + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Tin tức + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Bộ sưu tập sản phẩm + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ + +
+
\ No newline at end of file diff --git a/template/system/form_setup/pro_category.html b/template/system/form_setup/pro_category.html new file mode 100644 index 0000000..6a52fb4 --- /dev/null +++ b/template/system/form_setup/pro_category.html @@ -0,0 +1,121 @@ +
+
+ Hướng dẫn: +
    +
  • - ID danh mục: nhập danh sách các ID danh mục cách nhau dấu , hoặc khoảng trắng. + Để percat nếu muốn hệ thống + tự động lấy theo danh mục sản phẩm đang xem
  • +
  • - Số lượng hiển thị : Nhập 0 nếu không muốn dùng nội dung, điền số > 0 để có + số lượng cần lấy Để website chạy nhanh nhất, chỉ kích hoạt các nội dung mà giao diện hiển thị cần đến. +
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Sản phẩm nổi bật (HOT) + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Sản phẩm bán chạy nhất (BEST SALE) + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Sản phẩm mới nhất (NEW) + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Sản phẩm xả hàng (SALE OFF) + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Tin tức + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ Bộ sưu tập sản phẩm + +
+ ID danh mục + +
+
+ Số lượng hiển thị + +
+
+ + +
+
\ No newline at end of file diff --git a/template/system/form_setup/product.html b/template/system/form_setup/product.html new file mode 100644 index 0000000..c5c3945 --- /dev/null +++ b/template/system/form_setup/product.html @@ -0,0 +1,71 @@ +
+
+ + + + + + + + + + + + + + +
+ Kích thước ảnh sản phẩm + Điều chỉnh kích thước ảnh cho phù hợp với giao diện + website: + +
+ thumb + + px +
+
+ small + + px +
+
+ medium + + px +
+
+ large + + px +
+
+ Tạo ảnh vuông + +
+ + Tạo các ảnh nhỏ hình vuông với kích thước như trên + +
+
+ Ghi chữ bản quyền lên ảnh + +
+ + Ghi bản quyền +
+

(Tên miền website demopc8.hurasoft.com sẽ được ghi lên + ảnh sản + phẩm)

+
+ + +
+
\ No newline at end of file diff --git a/template/system/form_store_design/background.html b/template/system/form_store_design/background.html new file mode 100644 index 0000000..defcb14 --- /dev/null +++ b/template/system/form_store_design/background.html @@ -0,0 +1,21 @@ +
+ + + + + +
Thay Ảnh nền +
+ +
+ + + + Bạn có thể thay nền website bằng hình ảnh. Với file ảnh, yêu + cầu là .jpg, .gif, hoặc .png và dung lượng tối đa 300KB. +
+ + +
\ No newline at end of file diff --git a/template/system/form_store_design/header.html b/template/system/form_store_design/header.html new file mode 100644 index 0000000..9424a9b --- /dev/null +++ b/template/system/form_store_design/header.html @@ -0,0 +1,30 @@ +
+ + + + + + + + + + +
Thay Logo +
+ +
+ + +
Thay Ảnh header + +
+ +
+ + +
+ + +
\ No newline at end of file diff --git a/template/system/form_store_design/other.html b/template/system/form_store_design/other.html new file mode 100644 index 0000000..6ad7364 --- /dev/null +++ b/template/system/form_store_design/other.html @@ -0,0 +1,93 @@ +
+ + Cảnh báo: Vì các cài đặt sau sẽ được sử dụng ở biến global. Nên hệ thống sẽ cho phép tạo tối đa 20 giá + trị và giới hạn số ký tự không quá 500 mỗi giá trị để đảm bảo website hoạt động tốt. Do vậy chỉ cài đặt các nội + dung thật sự sẽ được hiển thị ở tất cả các nơi (v.d. Header/Footer). + + +
+ + + + + + + + + + + + + + + + + + + + + +
STT Code giao diện Chú thích (cho quản trị viên) Giá trị Hành động
1 global.settings.builpc_category_special_offer Danh sách khuyến mại theo từng danh mục trang Xây dựng cấu hình + + + Cập + nhật + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/template/system/form_store_design/popup.html b/template/system/form_store_design/popup.html new file mode 100644 index 0000000..3a30a89 --- /dev/null +++ b/template/system/form_store_design/popup.html @@ -0,0 +1,57 @@ +
+ + + + + + + + + + + + + + + + + + + + + +
Link nhảy đến + +
File ảnh +
+ +
+ + +
Lựa chọn hiển thị người dùng +
+ + + +
+
Khác + + +
+ + +
\ No newline at end of file diff --git a/template/system/pay_method.html b/template/system/pay_method.html new file mode 100644 index 0000000..3bf1246 --- /dev/null +++ b/template/system/pay_method.html @@ -0,0 +1,69 @@ + + +
+
+
+

Các phương thức thanh toán

+ + + Thêm phương thức mới +
+

Cập nhật các phương thức thanh toán mà quý khách muốn hiển thị ở trang thanh toán đơn + hàng. Chỉ các phương + thức ở trạng + thái "Đang hiển thị" được hiển thị ở trang thanh toán. +

+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + +
STTPhân loạiPhương thứcCập nhậtThứ tựLựa chọn
1codThanh toán khi nhận hàng (COD) (#1) +

25-09-2022, 2:42 pm

+

Hurasoft Test

+
10 +
+ + +
+
+
+
+
\ No newline at end of file diff --git a/template/system/pay_method_add.html b/template/system/pay_method_add.html new file mode 100644 index 0000000..7da6cf0 --- /dev/null +++ b/template/system/pay_method_add.html @@ -0,0 +1,96 @@ + + +
+
+

Các phương thức thanh toán

+

Tiền mặt khi nhận hàng (COD)

+
+ + + +
+
+
+ Phân loại (*) +
+ + + + + + + +
+
+
+ + +
+
+ + +
+
+
+ + (cao xếp trước) +
+ +
+
+ Phân loại (*) +
+ + +
+
+
+
+ +
+
+
\ No newline at end of file diff --git a/template/system/print.html b/template/system/print.html new file mode 100644 index 0000000..97e6b9b --- /dev/null +++ b/template/system/print.html @@ -0,0 +1,317 @@ + + +
+
+

Cài đặt In ấn (Print) và Xuất Excel

+

Hướng dẫn: Nên dùng Editor ở phần sửa + template để chỉnh nội dung html rồi + copy/cập + nhật ở đây +

+
+ + + +
+
+ Dành cho khách hàng: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTPhân loạiNội dungHành động
1 +

In giỏ hàng

+

print-user-cart

+
+ Xem + nội + dung + + + Cập + nhật +
2 +

In đơn hàng

+

print-user-order

+
+ Xem + nội + dung + + + Cập + nhật +
3 +

Xuất excel giỏ hàng

+

export-shopping-cart

+
+ Xem + nội + dung + + + Cập + nhật +
4 +

Xuất excel sản phẩm

+

export-product-quote

+
+ Xem + nội + dung + + + Cập + nhật +
5 +

Xuất xây dựng cấu hình máy tính

+

export-buildpc

+
+ Xem + nội + dung + + + Cập + nhật +
6 +

Xuất xây dựng tản nhiệt

+

export-tannhiet

+
+ Xem + nội + dung + + + Cập + nhật +
+
+
+ Dành cho quản trị viên website: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTPhân loạiNội dungHành động
1 + In đơn hàng trong admin
print-admin-order-detail-new +
+ Xem + nội + dung + + + Cập + nhật +
2 + In danh sách đơn hàng trong admin
print-admin-order-list +
+ Xem + nội + dung + + + Cập + nhật +
3 + In admin log
print-admin-admin-log +
+ Xem + nội + dung + + + Cập + nhật +
+ +
+
+
\ No newline at end of file diff --git a/template/system/province_list.html b/template/system/province_list.html new file mode 100644 index 0000000..35e730f --- /dev/null +++ b/template/system/province_list.html @@ -0,0 +1,78 @@ + + +
+
+

Thông tin các tỉnh

+
+ + + +
+
+ Danh sách các tỉnh + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTSystemIDTênĐịa chỉHuyện
16 + An Giang + + Huyện An Phú , Huyện Châu Phú , Huyện Châu Thành , Huyện Chợ Mới , Huyện Phú Tân , Huyện + Tịnh Biên , Huyện Thoại Sơn , + Huyện Tri Tôn , Thành phố Châu Đốc , Thành phố Long Xuyên , Thị xã Tân Châu + +
+

1

+ +
+
16 + An Giang + + Huyện An Phú , Huyện Châu Phú , Huyện Châu Thành , Huyện Chợ Mới , Huyện Phú Tân , Huyện + Tịnh Biên , Huyện Thoại Sơn , + Huyện Tri Tôn , Thành phố Châu Đốc , Thành phố Long Xuyên , Thị xã Tân Châu + +
+

1

+ +
+
+
+
+
\ No newline at end of file diff --git a/template/system/settings.html b/template/system/settings.html new file mode 100644 index 0000000..3ff40e0 --- /dev/null +++ b/template/system/settings.html @@ -0,0 +1,168 @@ +
+
+

Tổng hợp các cài đặt cho website

+
+ Cài đặt hiển thị ngoài giao diện: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTNội dungQuản lý
1Header (logo, banner top) + Quản + lý +
2Banner popup + Quản + lý +
3Ảnh/màu nền website + Quản + lý +
4Thông tin cài đặt tùy biến khác + Quản + lý +
+
+
+ Cài đặt dùng cho hệ thống: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTNội dungQuản lý
1Cài đặt hiển thị + Quản + lý +
2Chi nhánh cửa hàng + Quản + lý +
3Chặn IP xấu truy cập website + Quản + lý +
4Email bán hàng/CSKH + Quản + lý +
5Print/ Xuất Excel + Quản + lý +
6Meta SEO trang chủ + Quản + lý +
7Thông tin giới thiệu cửa hàng + Quản + lý +
8Thông tin các tỉnh/thành/quận-huyện + Quản + lý +
9Các phương thức thanh toán + Quản + lý +
10Các phương thức vận chuyển + Quản + lý +
11Cài đặt phí vận chuyển + Quản + lý +
+
+
+
\ No newline at end of file diff --git a/template/system/setup.html b/template/system/setup.html new file mode 100644 index 0000000..16793ef --- /dev/null +++ b/template/system/setup.html @@ -0,0 +1,58 @@ + + +
+
+

Cài đặt hiển thị

+
+ + + + + {% if global.url contains 'option=general' %} + + {% include 'system/form_setup/general' %} + + {% elsif global.url contains 'option=homepage' %} + + {% include 'system/form_setup/homepage' %} + + {% elsif global.url contains 'option=pro_category' %} + + {% include 'system/form_setup/pro_category' %} + + {% elsif global.url contains 'option=product' %} + + {% include 'system/form_setup/product' %} + + {% endif %} + +
\ No newline at end of file diff --git a/template/system/ship_method.html b/template/system/ship_method.html new file mode 100644 index 0000000..cf51cea --- /dev/null +++ b/template/system/ship_method.html @@ -0,0 +1,74 @@ +
+ + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTPhân loạiPhương thứcCập nhậtChỉnh sửa
1Nhận hàng tại cửa hàngGiao hàng nhanh trong 2 giờ (Nội thành Hà Nội +HCM) +

Admin

+

09-09-2022, 11:53 am

+
+
+ + +
+
1Nhận hàng tại cửa hàngGiao hàng nhanh trong 2 giờ (Nội thành Hà Nội +HCM) +

Admin

+

09-09-2022, 11:53 am

+
+
+ + +
+
+ +
+
\ No newline at end of file diff --git a/template/system/ship_method_add.html b/template/system/ship_method_add.html new file mode 100644 index 0000000..8015094 --- /dev/null +++ b/template/system/ship_method_add.html @@ -0,0 +1,104 @@ +
+ + +
+ + + +
+ +
+
+ Phân loại (*) + +
+ + + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+
+
+ +
+
+
+ +
+ +
\ No newline at end of file diff --git a/template/system/shipping.html b/template/system/shipping.html new file mode 100644 index 0000000..d0d50aa --- /dev/null +++ b/template/system/shipping.html @@ -0,0 +1,95 @@ + + +
+
+

Cài đặt hệ thống

+
+ + + +
+
+
+
+ + (* Đơn hàng có giá trị nhỏ hơn giá trị này sẽ được áp + dụng biểu + phí theo vùng) +
+ +
+
+
+ Cài đặt phí thu hộ + + + + + + + + + + + + + + + + + +
STTGiá trị đơn hàng dưới mứcPhí thu hộ (VND)Lựa chọn
1 + + + + + + + +
+ +
+
+ + +
+ Hướng dẫn cài phí thu hộ +

Phí thu hộ được tính theo giá trị cần thu hộ, ví dụ:

+

- Từ 0 đến <=1 triệu=30.000đ=> Cần nhập là 1.000.000

+

- Từ >1 triệu đến <= 2 triệu=50.000đ=> Cần nhập là 2.000.000

+

- Từ >2 triệu đến <= 3 triệu=70.000đ=> Cần nhập là 3.000.000

+

- Từ >3 triệu đến <= 4 triệu=90.000đ=> Cần nhập là 4.000.000

+

- Từ >4 triệu đến <= 5 triệu=110.000đ=> Cần nhập là 5.000.000

+

- Từ >5 triệu đến <= 6 triệu=130.000đ=> Cần nhập là 6.000.000

+

- Từ >6 triệu đến <= 7 triệu=150.000đ=> Cần nhập là 7.000.000

+

- Từ >7 triệu đến <= 8 triệu=170.000đ=> Cần nhập là 8.000.000

+

- Từ >8 triệu đến <= 9 triệu=190.000đ=> Cần nhập là 9.000.000

+

- Trên 9 triệu = 0đ => Cần nhập là 10.000.000.000 (nhập số lớn nhất có thể)

+

Để xóa phí nào nó, click "Xóa bỏ" và nhấn "Cập nhật" để lưu lại

+
+
+
+
\ No newline at end of file diff --git a/template/system/store_address.html b/template/system/store_address.html new file mode 100644 index 0000000..af7f9d0 --- /dev/null +++ b/template/system/store_address.html @@ -0,0 +1,141 @@ + + +
+
+

Các địa chỉ cửa hàng

+
+ + + +
+
+ Thêm địa chỉ mới +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ Đây là 1 cửa hàng ? + +
+ + + +
+ +
+ Danh sách địa chỉ cửa hàng + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTTên gọiĐịa chỉTỉnhLà 1 cửa hàngERP IDLựa chọn
1HACOM Q7, TP. Hồ Chí Minh500 Nguyễn Thị Thập - Quận 7 - TP. Hồ Chí MinhTP HCMyesTon120 +
+ + + +
+
1HACOM Q7, TP. Hồ Chí Minh500 Nguyễn Thị Thập - Quận 7 - TP. Hồ Chí MinhTP HCMyesTon120 +
+ + + +
+
+
+
+
\ No newline at end of file diff --git a/template/system/store_design.html b/template/system/store_design.html new file mode 100644 index 0000000..c1affc9 --- /dev/null +++ b/template/system/store_design.html @@ -0,0 +1,63 @@ + + +
+
+

Tùy chỉnh thành phần giao diện

+ +

+ Chú ý: Chức năng này chỉ áp dụng với các giao diện đã được cài đặt cho phép thay đổi 1 số thành phần + của giao diện. Biến template sử dụng $settings (global) +

+
+ + + + {% if global.url contains 'section=header' %} + + {% include 'system/form_store_design/header' %} + + {% elsif global.url contains 'section=popup' %} + + {% include 'system/form_store_design/popup' %} + + {% elsif global.url contains 'section=background' %} + + {% include 'system/form_store_design/background' %} + + {% elsif global.url contains 'section=other' %} + + {% include 'system/form_store_design/other' %} + + {% endif %} +
\ No newline at end of file diff --git a/template/tag/add.html b/template/tag/add.html new file mode 100644 index 0000000..af70212 --- /dev/null +++ b/template/tag/add.html @@ -0,0 +1,82 @@ +
+ + + +
+ + +
+
+ +
+
+
+ Thông tin tag +
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/template/tag/home.html b/template/tag/home.html new file mode 100644 index 0000000..1a69f1a --- /dev/null +++ b/template/tag/home.html @@ -0,0 +1,79 @@ +
+
+

Danh sách tag

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTTagLink đíchThời gian tạoLựa chọn
1ATO tesst +
+ + xem +
+
20-10-2023, 5:58 pm / Khách +
+ + +
+
1màn hình thời gián đáp ứng 5ms +
+ + xem +
+
20-10-2023, 5:58 pm / Khách +
+ + +
+
+ +
+
\ No newline at end of file diff --git a/template/template/add_template.html b/template/template/add_template.html new file mode 100644 index 0000000..c39b138 --- /dev/null +++ b/template/template/add_template.html @@ -0,0 +1,73 @@ + + +
+
+
+

Cập nhật template file, giao diện

+ +
+

mobile_detail

+
+ + + +
+
+ Cập nhật lại +
+
+ + v.d. product_detail, account_home +
+ +
+
+
+
+ + v.d. product / other / customer / ajax +
+ +
+
+ hoặc nhập + +
+
+
+
+ +
+ +
+
+
+ +
+
+
\ No newline at end of file diff --git a/template/template/edit_template.html b/template/template/edit_template.html new file mode 100644 index 0000000..fde3d48 --- /dev/null +++ b/template/template/edit_template.html @@ -0,0 +1,227 @@ +
+ + + +
+
+

Cập nhật template file, giao diện

+

web_mau

+
+ + +
+
+
+
+ +
+

Giao diện mobile

+ +
+
+
+
+
+ other + +
+
+ other + +
+
+ other + +
+
+ other + +
+
+ other + +
+
+ other + +
+
+
+
+
+ +
+

Giao diện pc

+ +
+
+
+
+
+ other + +
+
+ other + +
+
+ other + +
+
+
+
+
+
+
+ +
+

Giao diện Website

+ +
+
+ +
+
+
+
+ +
+

File .js & .css

+ +
+
+ +
+
+
+
+ +
+

Ảnh giao diện

+ +
+
+ +
+
+
+
+
+
+

about_us

+ Xem các bản + cũ +
+ + + +
+
+ +
+ + +
+ +
+
+ +
+ +
+
\ No newline at end of file diff --git a/template/template/home.html b/template/template/home.html new file mode 100644 index 0000000..b5d2f00 --- /dev/null +++ b/template/template/home.html @@ -0,0 +1,80 @@ +
+
+

Danh sách giao diện web

+ + + + + Thêm mẫu giao diện mới + + + + + + + + + + + + + + + + + + + + + + + + + +
STTThư mụcTên / miêu tảThông tinSửa giao diệnXem templateQuản lý
1default + Giao diện mặc định +

Mẫu giao diện ban đầu khi tạo website. Mẫu này được dùng khi không có mẫu nào khác được kích + hoạt sử dụng thay thế +

+ + Tải backup giao diện + +
+

Tạo lúc: 23-03-2021, 2:46 pm

+

Trần Khắc Tiệp

+
+ Sửa + giao diện + + + Danh sách file + + Upload file + + +
+
+
\ No newline at end of file diff --git a/template/template/set.html b/template/template/set.html new file mode 100644 index 0000000..6cb7c92 --- /dev/null +++ b/template/template/set.html @@ -0,0 +1,74 @@ + + +
+
+
+

Danh sách template file, giao diện

+ +
+ +

htpstore

+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + +
STTModuleTemplateThứ tựThông tinLựa chọn
10mobile_detail0Tạo lúc bởi: 0 + +
+
+
+
\ No newline at end of file diff --git a/template/template/upload_set.html b/template/template/upload_set.html new file mode 100644 index 0000000..7a3cb56 --- /dev/null +++ b/template/template/upload_set.html @@ -0,0 +1,54 @@ + + +
+
+

Cập nhật giao diện

+

htpstore

+
+ + + +
+
+ Cập nhật lại +
+
+ + v.d. xehoi (các file liên quan tới giao diện cần upload + tại + /template/xehoi/ ) +
+ +
+
+
+ + v.d. Giao diện xe hơi +
+ +
+
+ + +
+
+
+ +
+
+
\ No newline at end of file diff --git a/template/theme.html b/template/theme.html new file mode 100644 index 0000000..07ed9c8 --- /dev/null +++ b/template/theme.html @@ -0,0 +1,46 @@ + + + + + + + Admin Hura Pc + + + + + + + + + + + + + + + + {{global|show_var}} + {{component|show_var}} + {{banner|show_var}} + {{page|show_var}} + +
+ {% include "other/header" %} + +
+ {{ page_content }} +
+ +
+ +
+
+ + {% include javascript/index %} + + + + \ No newline at end of file diff --git a/template/tool/home.html b/template/tool/home.html new file mode 100644 index 0000000..8af2863 --- /dev/null +++ b/template/tool/home.html @@ -0,0 +1,70 @@ +
+
+

Công cụ

+

+ Lưu ý: Các công cụ dưới đây thực hiện thao tác áp dụng cho hàng loạt nội dung. Chỉ nên phân quyền cho + phép một số quản + trị viên được sử dụng để tránh dữ liệu bị xóa +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
STTCông cụLink
1 + Thao tác cho hàng loạt Sản phẩm +

Thực hiện 1 thao tác cho 1 danh sách các Sản phẩm, gồm: Xóa, Hiển thị, Đổi danh mục

+
+ Sử + dụng +
2 + Chuyển Sản phẩm giữa các danh mục +

Chuyển toàn bộ hoặc 1 nhóm Sản phẩm từ danh mục A sang danh mục B

+
+ Sử + dụng +
3 + Cập nhật sản phẩm chưa đủ thuộc tính +

Xem các sản phẩm chưa có đủ thuộc tính để cập nhật nhanh

+
+ Sử + dụng +
4 + Kiểm tra các mã sản phẩm (SKU) chưa có tại website +

Nhập các mã SKU để kiểm tra xem đã có trên website hay chưa

+
+ Sử + dụng +
+
+
\ No newline at end of file diff --git a/template/url/redirect.html b/template/url/redirect.html new file mode 100644 index 0000000..f6a88a9 --- /dev/null +++ b/template/url/redirect.html @@ -0,0 +1,72 @@ +
+
+

Danh sách link redirect

+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + +
STTLink truy cậpLink đíchRedirect_codeQuản lý
1 + + + + + 301 + + +
+ +
+
\ No newline at end of file diff --git a/template/url/redirect_form.html b/template/url/redirect_form.html new file mode 100644 index 0000000..7d0ec88 --- /dev/null +++ b/template/url/redirect_form.html @@ -0,0 +1,47 @@ + + +
+
+

Cập nhật link redirect

+

/bo-phim-chuot-fuhlen-l411-fuhlen-l102

+
+ + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
\ No newline at end of file diff --git a/template/url/url_seo.html b/template/url/url_seo.html new file mode 100644 index 0000000..71a22bc --- /dev/null +++ b/template/url/url_seo.html @@ -0,0 +1,74 @@ +
+
+ +
+

Upload Excel SEO sản phẩm

+ +

Yêu cầu file excel theo đúng Cấu hình cột Excel bên dưới

+
+ + +
+ +
+ + +
+ + + + + + + + + + + + +
File cập nhật + +
Cấu hình cột Excel + + + + + + + + + + + + + + + + + + + + + + + + + + +
Cột Excel Nội dung
A ID Sản phẩm Web
B Mã kho (SKU)
C Thẻ Meta Title
D Thẻ Meta Keyword
E Thẻ Meta Description
+ +
+ + +
+
+
\ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a7c5f50 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "nodenext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "moduleResolution": "node16", + "ignoreDeprecations": "6.0" + }, + "include": ["./assets/typescript/main.ts"], + "exclude": ["node_modules"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..9660543 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from "vite" +import { resolve } from "node:path" +import { fileURLToPath } from "node:url" + +const __dirname = fileURLToPath(new URL(".", import.meta.url)) + +export default defineConfig({ + build: { + lib: { + entry: { + global: resolve(__dirname, "assets/typescript/main.ts"), + }, + formats: ["es"], + fileName: (format, entryName) => `${entryName}.js`, + }, + outDir: "assets/script", + emptyOutDir: false, + rollupOptions: { + output: { + dir: "assets/script", + entryFileNames: "[name].js", + }, + }, + }, +})