first commit
This commit is contained in:
18
extensions/fiscal-checkout-block/locales/en.default.json
Normal file
18
extensions/fiscal-checkout-block/locales/en.default.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Fiscal Order Status Block",
|
||||
"hutko": {
|
||||
"fiscal_receipts": "Fiscal Receipts",
|
||||
"loading": "Loading fiscal receipts...",
|
||||
"no_receipts": "No fiscal receipts found.",
|
||||
"view_receipt": "View Receipt",
|
||||
"download_xml": "Download XML",
|
||||
"tax_service": "Tax Service",
|
||||
"provider": "Provider",
|
||||
"pdf": "PDF",
|
||||
"waiting_for_fiscalisation": "Receipt {{id}} — waiting for fiscalisation",
|
||||
"failed_to_load_receipts": "Failed to load fiscal receipts. Please try again.",
|
||||
"currency": "UAH",
|
||||
"refresh": "Refresh",
|
||||
"fiscal_number_label": "FN: "
|
||||
}
|
||||
}
|
||||
18
extensions/fiscal-checkout-block/locales/uk.json
Normal file
18
extensions/fiscal-checkout-block/locales/uk.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Блок статусу фіскального замовлення",
|
||||
"hutko": {
|
||||
"fiscal_receipts": "Фіскальні чеки",
|
||||
"loading": "Завантаження фіскальних чеків...",
|
||||
"no_receipts": "Фіскальних чеків не знайдено.",
|
||||
"view_receipt": "Переглянути чек",
|
||||
"download_xml": "Завантажити XML",
|
||||
"tax_service": "Податкова служба",
|
||||
"provider": "Провайдер",
|
||||
"pdf": "PDF",
|
||||
"waiting_for_fiscalisation": "Чек {{id}} — очікує фіскалізації",
|
||||
"failed_to_load_receipts": "Не вдалося завантажити фіскальні чеки. Будь ласка, спробуйте ще раз.",
|
||||
"currency": "грн",
|
||||
"refresh": "Оновити",
|
||||
"fiscal_number_label": "ФН: "
|
||||
}
|
||||
}
|
||||
7
extensions/fiscal-checkout-block/shopify.d.ts
vendored
Normal file
7
extensions/fiscal-checkout-block/shopify.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import '@shopify/ui-extensions';
|
||||
|
||||
//@ts-ignore
|
||||
declare module './src/OrderStatusExtension.jsx' {
|
||||
const shopify: import('@shopify/ui-extensions/customer-account.order-status.block.render').Api;
|
||||
const globalThis: { shopify: typeof shopify };
|
||||
}
|
||||
17
extensions/fiscal-checkout-block/shopify.extension.toml
Normal file
17
extensions/fiscal-checkout-block/shopify.extension.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
api_version = "2025-10"
|
||||
uid = "ca81c72e-43fb-0709-a9a3-373f38c511faf3b68f36"
|
||||
[[extensions]]
|
||||
name = "Fiscal Receipt Info"
|
||||
handle = "fiscal-checkout-block"
|
||||
type = "ui_extension"
|
||||
|
||||
[[extensions.targeting]]
|
||||
module = "./src/OrderStatusExtension.jsx"
|
||||
target = "customer-account.order-status.block.render"
|
||||
|
||||
[extensions.capabilities]
|
||||
network_access = true
|
||||
|
||||
[[extensions.metafields]]
|
||||
namespace = "hutko"
|
||||
key = "fiscal_receipts"
|
||||
293
extensions/fiscal-checkout-block/src/OrderStatusExtension.jsx
Normal file
293
extensions/fiscal-checkout-block/src/OrderStatusExtension.jsx
Normal file
@@ -0,0 +1,293 @@
|
||||
import '@shopify/ui-extensions/customer-account';
|
||||
import { render } from 'preact';
|
||||
import { useState, useEffect, useCallback } from 'preact/hooks';
|
||||
|
||||
function openBase64AsBlob(base64String, mimeType) {
|
||||
try {
|
||||
const decoded = atob(base64String);
|
||||
const bytes = new Uint8Array(decoded.length);
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
bytes[i] = decoded.charCodeAt(i);
|
||||
}
|
||||
const blob = new Blob([bytes], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const win = window.open(url, '_blank');
|
||||
if (win) {
|
||||
setTimeout(() => URL.revokeObjectURL(url), 10000);
|
||||
}
|
||||
return !!win;
|
||||
} catch (e) {
|
||||
console.error('Failed to open blob:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function downloadBase64AsFile(base64String, filename, mimeType) {
|
||||
try {
|
||||
const decoded = atob(base64String);
|
||||
const bytes = new Uint8Array(decoded.length);
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
bytes[i] = decoded.charCodeAt(i);
|
||||
}
|
||||
const blob = new Blob([bytes], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
||||
} catch (e) {
|
||||
console.error('Failed to download file:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function queryCustomerAccountAPI(query, variables = {}) {
|
||||
const res = await fetch(
|
||||
'shopify://customer-account/api/2025-10/graphql.json',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query, variables }),
|
||||
}
|
||||
);
|
||||
if (!res.ok) throw new Error(`API request failed: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export default function extension() {
|
||||
render(<Extension />, document.body);
|
||||
}
|
||||
|
||||
function Extension() {
|
||||
const translate = shopify.i18n.translate;
|
||||
// Get order ID from the order status context signal
|
||||
const orderId = shopify.order?.value?.id;
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [receipts, setReceipts] = useState([]);
|
||||
|
||||
const fetchFiscalData = useCallback(async () => {
|
||||
if (!orderId) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
// Step 1: Read fiscal_receipts from pre-loaded appMetafields (declared in TOML)
|
||||
let receiptIds = [];
|
||||
const metafields = shopify.appMetafields?.value;
|
||||
if (metafields && metafields.length > 0) {
|
||||
const entry = metafields.find(
|
||||
(e) =>
|
||||
String(e.target?.type) === 'order' &&
|
||||
e.metafield?.namespace === 'hutko' &&
|
||||
e.metafield?.key === 'fiscal_receipts',
|
||||
);
|
||||
if (entry?.metafield?.value && typeof entry.metafield.value === 'string') {
|
||||
try {
|
||||
receiptIds = JSON.parse(entry.metafield.value);
|
||||
} catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: query via Customer Account API if appMetafields didn't have it
|
||||
if (receiptIds.length === 0) {
|
||||
const indexRes = await queryCustomerAccountAPI(
|
||||
`query getOrderMetafield($id: ID!) {
|
||||
order(id: $id) {
|
||||
metafield(namespace: "hutko", key: "fiscal_receipts") { value }
|
||||
}
|
||||
}`,
|
||||
{ id: orderId },
|
||||
);
|
||||
|
||||
try {
|
||||
receiptIds = JSON.parse(
|
||||
indexRes?.data?.order?.metafield?.value || '[]'
|
||||
);
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
if (receiptIds.length === 0) {
|
||||
setReceipts([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Fetch per-receipt info & storage via Customer Account API
|
||||
const detailed = await Promise.all(
|
||||
receiptIds.map(async (id) => {
|
||||
const res = await queryCustomerAccountAPI(
|
||||
`query getReceiptData($orderId: ID!, $infoKey: String!, $storageKey: String!) {
|
||||
order(id: $orderId) {
|
||||
info: metafield(namespace: "hutko", key: $infoKey) { value }
|
||||
storage: metafield(namespace: "hutko", key: $storageKey) { value }
|
||||
}
|
||||
}`,
|
||||
{
|
||||
orderId,
|
||||
infoKey: `fiscal_info_${id}`,
|
||||
storageKey: `fiscal_storage_${id}`,
|
||||
},
|
||||
);
|
||||
|
||||
let info = {};
|
||||
try {
|
||||
info = JSON.parse(res?.data?.order?.info?.value || '{}');
|
||||
} catch (e) { }
|
||||
|
||||
let storage = {};
|
||||
try {
|
||||
storage = JSON.parse(res?.data?.order?.storage?.value || '{}');
|
||||
} catch (e) { }
|
||||
|
||||
return { id, info, storage };
|
||||
})
|
||||
);
|
||||
|
||||
setReceipts(detailed);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch fiscal data:', e);
|
||||
setError(translate('hutko.failed_to_load_receipts'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [orderId, translate]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFiscalData();
|
||||
|
||||
const t1 = setTimeout(fetchFiscalData, 5000);
|
||||
const t2 = setTimeout(fetchFiscalData, 20000);
|
||||
const t3 = setTimeout(fetchFiscalData, 40000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(t1);
|
||||
clearTimeout(t2);
|
||||
clearTimeout(t3);
|
||||
};
|
||||
}, [fetchFiscalData]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<s-box padding="base" border="base" borderRadius="base">
|
||||
<s-stack direction="inline" alignItems="center" gap="base">
|
||||
<s-spinner size="small"></s-spinner>
|
||||
<s-text>{translate('hutko.loading')}</s-text>
|
||||
</s-stack>
|
||||
</s-box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <s-banner tone="critical">{error}</s-banner>;
|
||||
}
|
||||
|
||||
if (receipts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const logoSrc = "data:image/svg+xml,%3Csvg width='120' height='30' viewBox='188 423 624 154' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M747.38 473.832C762.386 473.832 774.926 478.66 784.944 488.305C794.962 497.957 800 509.817 800 523.877C800 537.938 794.962 549.594 784.944 559.246C774.876 568.897 762.386 573.72 747.38 573.72C732.374 573.72 719.898 568.947 709.923 559.344C699.955 549.742 694.967 537.938 694.967 523.871C694.967 509.803 700.005 498.104 710.023 488.403C720.095 478.713 732.504 473.835 747.38 473.824V473.832ZM474.833 530.079C474.833 535.826 476.46 540.445 479.713 543.992C482.966 547.531 487.373 549.332 492.984 549.333C499.491 549.333 504.687 547.433 508.621 543.634C512.555 539.835 514.547 534.648 514.547 528.032V475.729H540.883V571.51L540.832 571.461H514.705V561.963C507.467 569.407 497.972 573.15 486.27 573.15C474.568 573.15 465.653 569.61 458.781 562.524C451.909 555.438 448.498 546.102 448.498 534.55V475.721H474.833V530.079ZM266.725 535.497L322.589 501.641L256.912 571.921L255.929 543.519L200.008 571.921H200L269.09 426.28L266.725 535.497ZM375.332 485.276C382.57 477.832 392.065 474.089 403.768 474.089C415.47 474.089 424.384 477.629 431.256 484.715C438.128 491.802 441.539 501.138 441.539 512.689V571.517H415.204V517.161C415.204 511.413 413.577 506.794 410.324 503.248C407.071 499.708 402.664 497.907 397.053 497.907C390.546 497.907 385.35 499.806 381.416 503.605C377.482 507.404 375.49 512.591 375.49 519.208V571.51H349.154V475.729L349.205 452.648H375.332V485.276ZM588.437 475.729H611.991V498.88H588.443V537.326C588.443 541.531 589.44 544.616 591.482 546.459C593.532 548.31 596.836 549.284 601.407 549.284H611.991V571.51H599.508C574.434 571.51 561.943 560.576 561.943 538.658V498.929H547.833V475.729H561.943V452.647H588.437V475.729ZM645.649 518.639H645.707L678.126 475.729H708.238L670.674 523.315L709.75 571.461H676.75L645.649 532.09V571.461H618.941V475.729H645.649V518.639ZM747.38 497.389C739.877 497.389 733.635 499.905 728.598 504.938C723.567 509.971 721.044 516.279 721.044 523.877C721.044 531.475 723.517 537.994 728.39 543.027C733.27 548.059 739.404 550.569 746.807 550.569C754.209 550.569 760.666 548.003 765.904 542.921C771.1 537.84 773.722 531.475 773.722 523.877C773.722 516.279 771.207 509.964 766.169 504.938H766.162C761.132 499.905 754.883 497.389 747.38 497.389Z' fill='%23E40B2D'/%3E%3C/svg%3E";
|
||||
|
||||
return (<s-box>
|
||||
<s-stack gap="base" padding="base" border="base" borderRadius="base">
|
||||
|
||||
<s-heading>{translate('hutko.fiscal_receipts')}</s-heading>
|
||||
|
||||
{receipts.map((receipt) => (
|
||||
<s-box key={receipt.id} padding="base" border="base" borderRadius="base">
|
||||
<s-stack gap="base">
|
||||
<s-stack direction="inline" justifyContent="space-between" alignItems="center">
|
||||
<s-stack gap="none">
|
||||
{receipt.info.display_datetime ? (
|
||||
<>
|
||||
<s-text type="strong">{receipt.info.display_datetime}</s-text>
|
||||
{receipt.info.amount && (
|
||||
<s-text>{receipt.info.amount} {translate('hutko.currency')}</s-text>
|
||||
)}
|
||||
{receipt.info.fiscal_number && (
|
||||
<s-text type="small">{translate('hutko.fiscal_number_label')}{receipt.info.fiscal_number}</s-text>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<s-text>{translate('hutko.waiting_for_fiscalisation', { id: receipt.id })}</s-text>
|
||||
)}
|
||||
</s-stack>
|
||||
</s-stack>
|
||||
|
||||
<s-divider></s-divider>
|
||||
|
||||
<s-stack direction="inline" gap="base">
|
||||
{receipt.storage.html && (
|
||||
<s-button
|
||||
onClick={() =>
|
||||
openBase64AsBlob(receipt.storage.html, 'text/html')
|
||||
}
|
||||
>
|
||||
{translate('hutko.view_receipt')}
|
||||
</s-button>
|
||||
)}
|
||||
{receipt.storage.xml && (
|
||||
<s-button
|
||||
onClick={() =>
|
||||
downloadBase64AsFile(
|
||||
receipt.storage.xml,
|
||||
`fiscal_receipt_${receipt.id}.xml`,
|
||||
'application/xml'
|
||||
)
|
||||
}
|
||||
>
|
||||
{translate('hutko.download_xml')}
|
||||
</s-button>
|
||||
)}
|
||||
</s-stack>
|
||||
|
||||
{receipt.info.links && (
|
||||
<s-stack direction="inline" gap="base">
|
||||
{receipt.info.links.tax_service && (
|
||||
<s-link href={receipt.info.links.tax_service} target="_blank">
|
||||
{translate('hutko.tax_service')}
|
||||
</s-link>
|
||||
)}
|
||||
{receipt.info.links.external_provider && (
|
||||
<s-link href={receipt.info.links.external_provider} target="_blank">
|
||||
{translate('hutko.provider')}
|
||||
</s-link>
|
||||
)}
|
||||
{receipt.info.links.pdf && (
|
||||
<s-link href={receipt.info.links.pdf} target="_blank">
|
||||
{translate('hutko.pdf')}
|
||||
</s-link>
|
||||
)}
|
||||
</s-stack>
|
||||
)}
|
||||
</s-stack>
|
||||
</s-box>
|
||||
))}
|
||||
<s-stack direction="inline" justifyContent="space-between" alignItems="center">
|
||||
<s-image
|
||||
src={logoSrc}
|
||||
alt="Hutko"
|
||||
aspectRatio="4/1"
|
||||
objectFit="contain"
|
||||
sizes="120px"
|
||||
inlineSize="auto"
|
||||
loading="lazy"
|
||||
></s-image>
|
||||
<s-stack direction="inline" justifyContent="end">
|
||||
<s-button
|
||||
onClick={fetchFiscalData}
|
||||
disabled={loading}
|
||||
>
|
||||
{translate('hutko.refresh')}
|
||||
</s-button>
|
||||
</s-stack>
|
||||
</s-stack>
|
||||
</s-stack></s-box>
|
||||
);
|
||||
}
|
||||
|
||||
13
extensions/fiscal-checkout-block/tsconfig.json
Normal file
13
extensions/fiscal-checkout-block/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
"target": "ES2020",
|
||||
"checkJs": true,
|
||||
"allowJs": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
5
extensions/fiscal-order-block/README.md
Normal file
5
extensions/fiscal-order-block/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Block Extension
|
||||
|
||||
Admin block extensions enable app developers to build custom functionality into the context of Shopify Admin. These extensions surface as cards at various extension targets and enhance the merchant experience. Developers can build the content of these extensions using Shopify's UI Extension components for Admin.
|
||||
|
||||
Learn more about Admin block extensions in Shopify’s [developer documentation](https://shopify.dev/docs/apps/admin/admin-actions-and-blocks).
|
||||
20
extensions/fiscal-order-block/locales/en.default.json
Normal file
20
extensions/fiscal-order-block/locales/en.default.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Fiscal Order Block",
|
||||
"hutko": {
|
||||
"fiscal_receipts_title": "Hutko Fiscal Receipts",
|
||||
"fiscal_receipts": "Fiscal Receipts",
|
||||
"refresh": "Refresh",
|
||||
"loading": "Loading...",
|
||||
"no_receipts": "No fiscal receipts found for this order.",
|
||||
"view_receipt": "View Receipt",
|
||||
"download_xml": "Download XML",
|
||||
"tax_service": "Tax Service",
|
||||
"provider": "Provider",
|
||||
"pdf": "PDF",
|
||||
"waiting_for_fiscalisation": "Receipt {{id}} — waiting for fiscalisation",
|
||||
"no_order_selected": "No order selected.",
|
||||
"failed_to_load_receipts": "Failed to load fiscal receipts: ",
|
||||
"currency": "UAH",
|
||||
"customer_actions": "Customer actions"
|
||||
}
|
||||
}
|
||||
20
extensions/fiscal-order-block/locales/uk.json
Normal file
20
extensions/fiscal-order-block/locales/uk.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Блок фіскальних чеків",
|
||||
"hutko": {
|
||||
"fiscal_receipts_title": "Фіскальні чеки Hutko",
|
||||
"fiscal_receipts": "Фіскальні чеки",
|
||||
"refresh": "Оновити",
|
||||
"loading": "Завантаження...",
|
||||
"no_receipts": "Для цього замовлення не знайдено фіскальних чеків.",
|
||||
"view_receipt": "Переглянути чек",
|
||||
"download_xml": "Завантажити XML",
|
||||
"tax_service": "Податкова служба",
|
||||
"provider": "Провайдер",
|
||||
"pdf": "PDF",
|
||||
"waiting_for_fiscalisation": "Чек {{id}} — очікує фіскалізації",
|
||||
"no_order_selected": "Замовлення не вибрано.",
|
||||
"failed_to_load_receipts": "Не вдалося завантажити фіскальні чеки: ",
|
||||
"currency": "грн",
|
||||
"customer_actions": "Дії клієнта"
|
||||
}
|
||||
}
|
||||
7
extensions/fiscal-order-block/shopify.d.ts
vendored
Normal file
7
extensions/fiscal-order-block/shopify.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import '@shopify/ui-extensions';
|
||||
|
||||
//@ts-ignore
|
||||
declare module './src/BlockExtension.jsx' {
|
||||
const shopify: import('@shopify/ui-extensions/admin.order-details.block.render').Api;
|
||||
const globalThis: { shopify: typeof shopify };
|
||||
}
|
||||
13
extensions/fiscal-order-block/shopify.extension.toml
Normal file
13
extensions/fiscal-order-block/shopify.extension.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
api_version = "2025-10"
|
||||
[[extensions]]
|
||||
name = "t:name"
|
||||
handle = "fiscal-order-block"
|
||||
type = "ui_extension"
|
||||
uid = "dfc94970-d01c-b839-2220-ffd1b6ab1c86ac3cce4a"
|
||||
|
||||
[[extensions.targeting]]
|
||||
module = "./src/BlockExtension.jsx"
|
||||
target = "admin.order-details.block.render"
|
||||
|
||||
[extensions.capabilities]
|
||||
network_access = true
|
||||
252
extensions/fiscal-order-block/src/BlockExtension.jsx
Normal file
252
extensions/fiscal-order-block/src/BlockExtension.jsx
Normal file
@@ -0,0 +1,252 @@
|
||||
import '@shopify/ui-extensions/preact';
|
||||
import { render } from 'preact';
|
||||
import { useState, useEffect, useCallback } from 'preact/hooks';
|
||||
|
||||
function openBase64AsBlob(base64String, mimeType) {
|
||||
try {
|
||||
const decoded = atob(base64String);
|
||||
const bytes = new Uint8Array(decoded.length);
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
bytes[i] = decoded.charCodeAt(i);
|
||||
}
|
||||
const blob = new Blob([bytes], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const win = window.open(url, '_blank');
|
||||
if (win) {
|
||||
setTimeout(() => URL.revokeObjectURL(url), 10000);
|
||||
}
|
||||
return !!win;
|
||||
} catch (e) {
|
||||
console.error('Failed to open blob:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function downloadBase64AsFile(base64String, filename, mimeType) {
|
||||
try {
|
||||
const decoded = atob(base64String);
|
||||
const bytes = new Uint8Array(decoded.length);
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
bytes[i] = decoded.charCodeAt(i);
|
||||
}
|
||||
const blob = new Blob([bytes], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
||||
} catch (e) {
|
||||
console.error('Failed to download file:', e);
|
||||
}
|
||||
}
|
||||
|
||||
export default function extension() {
|
||||
render(<Extension />, document.body);
|
||||
}
|
||||
|
||||
function Extension() {
|
||||
const translate = shopify.i18n.translate;
|
||||
const orderId = shopify.data.selected?.[0]?.id;
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [receipts, setReceipts] = useState([]);
|
||||
|
||||
const fetchFiscalData = useCallback(async () => {
|
||||
if (!orderId) {
|
||||
setError(translate('hutko.no_order_selected'));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const indexRes = await fetch('shopify:admin/api/graphql.json', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
query getOrderMetafields($id: ID!) {
|
||||
order(id: $id) {
|
||||
receipts: metafield(namespace: "hutko", key: "fiscal_receipts") { value }
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { id: orderId }
|
||||
})
|
||||
});
|
||||
const indexData = await indexRes.json();
|
||||
|
||||
const receiptIds = JSON.parse(
|
||||
indexData?.data?.order?.receipts?.value || '[]'
|
||||
);
|
||||
|
||||
if (receiptIds.length === 0) {
|
||||
setReceipts([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const detailed = await Promise.all(
|
||||
receiptIds.map(async (id) => {
|
||||
const res = await fetch('shopify:admin/api/graphql.json', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
query getReceiptData($id: ID!, $infoKey: String!, $storageKey: String!) {
|
||||
order(id: $id) {
|
||||
info: metafield(namespace: "hutko", key: $infoKey) { value }
|
||||
storage: metafield(namespace: "hutko", key: $storageKey) { value }
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: orderId,
|
||||
infoKey: `fiscal_info_${id}`,
|
||||
storageKey: `fiscal_storage_${id}`,
|
||||
}
|
||||
})
|
||||
});
|
||||
const resData = await res.json();
|
||||
|
||||
let info = {};
|
||||
try {
|
||||
info = JSON.parse(resData?.data?.order?.info?.value || '{}');
|
||||
} catch (e) { }
|
||||
|
||||
let storage = {};
|
||||
try {
|
||||
storage = JSON.parse(resData?.data?.order?.storage?.value || '{}');
|
||||
} catch (e) { }
|
||||
|
||||
return { id, info, storage };
|
||||
})
|
||||
);
|
||||
|
||||
setReceipts(detailed);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch fiscal data:', e);
|
||||
setError(translate('hutko.failed_to_load_receipts') + e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [orderId, translate]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFiscalData();
|
||||
}, [fetchFiscalData]);
|
||||
const logoSrc = "data:image/svg+xml,%3Csvg width='120' height='30' viewBox='188 423 624 154' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M747.38 473.832C762.386 473.832 774.926 478.66 784.944 488.305C794.962 497.957 800 509.817 800 523.877C800 537.938 794.962 549.594 784.944 559.246C774.876 568.897 762.386 573.72 747.38 573.72C732.374 573.72 719.898 568.947 709.923 559.344C699.955 549.742 694.967 537.938 694.967 523.871C694.967 509.803 700.005 498.104 710.023 488.403C720.095 478.713 732.504 473.835 747.38 473.824V473.832ZM474.833 530.079C474.833 535.826 476.46 540.445 479.713 543.992C482.966 547.531 487.373 549.332 492.984 549.333C499.491 549.333 504.687 547.433 508.621 543.634C512.555 539.835 514.547 534.648 514.547 528.032V475.729H540.883V571.51L540.832 571.461H514.705V561.963C507.467 569.407 497.972 573.15 486.27 573.15C474.568 573.15 465.653 569.61 458.781 562.524C451.909 555.438 448.498 546.102 448.498 534.55V475.721H474.833V530.079ZM266.725 535.497L322.589 501.641L256.912 571.921L255.929 543.519L200.008 571.921H200L269.09 426.28L266.725 535.497ZM375.332 485.276C382.57 477.832 392.065 474.089 403.768 474.089C415.47 474.089 424.384 477.629 431.256 484.715C438.128 491.802 441.539 501.138 441.539 512.689V571.517H415.204V517.161C415.204 511.413 413.577 506.794 410.324 503.248C407.071 499.708 402.664 497.907 397.053 497.907C390.546 497.907 385.35 499.806 381.416 503.605C377.482 507.404 375.49 512.591 375.49 519.208V571.51H349.154V475.729L349.205 452.648H375.332V485.276ZM588.437 475.729H611.991V498.88H588.443V537.326C588.443 541.531 589.44 544.616 591.482 546.459C593.532 548.31 596.836 549.284 601.407 549.284H611.991V571.51H599.508C574.434 571.51 561.943 560.576 561.943 538.658V498.929H547.833V475.729H561.943V452.647H588.437V475.729ZM645.649 518.639H645.707L678.126 475.729H708.238L670.674 523.315L709.75 571.461H676.75L645.649 532.09V571.461H618.941V475.729H645.649V518.639ZM747.38 497.389C739.877 497.389 733.635 499.905 728.598 504.938C723.567 509.971 721.044 516.279 721.044 523.877C721.044 531.475 723.517 537.994 728.39 543.027C733.27 548.059 739.404 550.569 746.807 550.569C754.209 550.569 760.666 548.003 765.904 542.921C771.1 537.84 773.722 531.475 773.722 523.877C773.722 516.279 771.207 509.964 766.169 504.938H766.162C761.132 499.905 754.883 497.389 747.38 497.389Z' fill='%23E40B2D'/%3E%3C/svg%3E";
|
||||
|
||||
return (
|
||||
|
||||
<s-admin-block heading={translate('hutko.fiscal_receipts_title')}>
|
||||
<s-stack gap="base">
|
||||
|
||||
|
||||
|
||||
{error && (
|
||||
<s-banner tone="critical">{error}</s-banner>
|
||||
)}
|
||||
|
||||
{loading && !error && (
|
||||
<s-box padding="base">
|
||||
<s-text>{translate('hutko.loading')}</s-text>
|
||||
</s-box>
|
||||
)}
|
||||
|
||||
{!loading && receipts.length === 0 && !error && (
|
||||
<s-banner tone="info">{translate('hutko.no_receipts')}</s-banner>
|
||||
)}
|
||||
|
||||
{!loading && receipts.length > 0 && receipts.map((receipt) => (
|
||||
<s-stack key={receipt.id} gap="base">
|
||||
<s-stack direction="inline" justifyContent="space-between" block-align="center">
|
||||
|
||||
{receipt.info.display_datetime ? (
|
||||
<>
|
||||
<s-chip font-weight="bold" ><s-icon slot="graphic" type="calendar-time" size="small"></s-icon> {receipt.info.display_datetime}</s-chip>
|
||||
{receipt.info.amount && (
|
||||
<s-chip><s-icon slot="graphic" type="money" size="small"></s-icon> {receipt.info.amount} {translate('hutko.currency')}</s-chip>
|
||||
)}
|
||||
<s-button commandFor="customer-menu" icon="menu-horizontal"></s-button>
|
||||
|
||||
<s-menu id="customer-menu" accessibilityLabel={translate('hutko.customer_actions')}>
|
||||
{receipt.storage.html && (
|
||||
<s-button icon="view"
|
||||
onClick={() =>
|
||||
openBase64AsBlob(receipt.storage.html, 'text/html')
|
||||
}
|
||||
>
|
||||
{translate('hutko.view_receipt')}
|
||||
</s-button>
|
||||
)}
|
||||
{receipt.storage.xml && (
|
||||
<s-button icon="code"
|
||||
onClick={() =>
|
||||
downloadBase64AsFile(
|
||||
receipt.storage.xml,
|
||||
`fiscal_receipt_${receipt.id}.xml`,
|
||||
'application/xml'
|
||||
)
|
||||
}
|
||||
>
|
||||
{translate('hutko.download_xml')}
|
||||
</s-button>
|
||||
)}
|
||||
{receipt.info.links && (
|
||||
<>
|
||||
{receipt.info.links.tax_service && (
|
||||
<s-button href={receipt.info.links.tax_service} target="_blank" icon="external">
|
||||
{translate('hutko.tax_service')}
|
||||
</s-button>
|
||||
)}
|
||||
{receipt.info.links.external_provider && (
|
||||
<s-button href={receipt.info.links.external_provider} target="_blank" icon="external">
|
||||
{translate('hutko.provider')}
|
||||
</s-button>
|
||||
)}
|
||||
{receipt.info.links.pdf && (
|
||||
<s-button href={receipt.info.links.pdf} target="_blank" icon="external">
|
||||
{translate('hutko.pdf')}
|
||||
</s-button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</s-menu>
|
||||
</>
|
||||
) : (
|
||||
<s-text>{translate('hutko.waiting_for_fiscalisation', { id: receipt.id })}</s-text>
|
||||
)}
|
||||
|
||||
</s-stack>
|
||||
|
||||
<s-divider />
|
||||
|
||||
|
||||
|
||||
|
||||
</s-stack>
|
||||
|
||||
))}
|
||||
<s-stack direction="inline" justifyContent="space-between" alignItems="center"><s-image
|
||||
src={logoSrc}
|
||||
alt="Hutko"
|
||||
sizes="120px"
|
||||
aspectRatio="4/1"
|
||||
inlineSize="auto"
|
||||
objectFit="contain"
|
||||
loading="lazy"
|
||||
></s-image>
|
||||
<s-stack direction="inline" inline-alignment="end">
|
||||
|
||||
<s-button onClick={fetchFiscalData} disabled={loading} icon="refresh"></s-button>
|
||||
|
||||
</s-stack></s-stack>
|
||||
</s-stack>
|
||||
</s-admin-block>
|
||||
|
||||
);
|
||||
}
|
||||
13
extensions/fiscal-order-block/tsconfig.json
Normal file
13
extensions/fiscal-order-block/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
"target": "ES2020",
|
||||
"checkJs": true,
|
||||
"allowJs": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user