@africanies/angular-web-sdk 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.angular/cache/22.0.9/playground/.tsbuildinfo +1 -1
- package/.angular/cache/22.0.9/playground/angular-compiler.db +0 -0
- package/.angular/cache/22.0.9/playground/angular-compiler.db-lock +0 -0
- package/.github/workflows/deploy-demo.yml +63 -0
- package/.nx/cache/run.json +14 -14
- package/.nx/cache/terminalOutputs/13524438527920092277 +196 -21
- package/.nx/cache/terminalOutputs/14153928176747262656 +45 -21
- package/.nx/cache/terminalOutputs/14416694322692463866 +108 -21
- package/.nx/cache/terminalOutputs/15255861479705594703 +51 -21
- package/.nx/cache/terminalOutputs/18132311043100376919 +315 -21
- package/.nx/cache/terminalOutputs/2436781291243085315 +8 -0
- package/.nx/cache/terminalOutputs/6664915487114632770 +44 -21
- package/.nx/cache/terminalOutputs/8767335728598176888 +37 -0
- package/.nx/cache/terminalOutputs/9739655320223921719 +47 -0
- package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db +0 -0
- package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db-shm +0 -0
- package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db-wal +0 -0
- package/.nx/workspace-data/d/daemon.log +1683 -0
- package/.nx/workspace-data/d/server-process.json +2 -2
- package/.nx/workspace-data/file-map.json +475 -518
- package/.nx/workspace-data/nx_files.nxt +0 -0
- package/.nx/workspace-data/project-graph.json +6 -6
- package/.nx/workspace-data/project-graph.lock +0 -0
- package/.nx/workspace-data/source-maps.json +80 -0
- package/.pnpm-store/v11/index.db +0 -0
- package/README.md +14 -2
- package/apps/playground/src/app/app.config.ts +28 -3
- package/dist/apps/playground/3rdpartylicenses.txt +415 -0
- package/dist/apps/playground/browser/assets/africanies-icons/icons.sprite.svg +1844 -0
- package/dist/apps/playground/browser/assets/africanies-ui/brand/africanies-logo-mini.png +0 -0
- package/dist/apps/playground/browser/assets/africanies-ui/brand/africanies-logo.png +0 -0
- package/dist/apps/playground/browser/assets/africanies-ui/brand/africanies-logo.svg +5 -0
- package/dist/apps/playground/browser/assets/africanies-ui/carriers/dhl.svg +7 -0
- package/dist/apps/playground/browser/chunk--VJ6U9Ev.js +2 -0
- package/dist/apps/playground/browser/chunk-B1QVY0hA.js +1 -0
- package/dist/apps/playground/browser/chunk-BHI7MG2Y.js +1 -0
- package/dist/apps/playground/browser/chunk-BJ5IK8Re.js +1 -0
- package/dist/apps/playground/browser/chunk-BOSfyWK2.js +1 -0
- package/dist/apps/playground/browser/chunk-BYPboEAg.js +1 -0
- package/dist/apps/playground/browser/chunk-BZBRtbFJ.js +1 -0
- package/dist/apps/playground/browser/chunk-BdrGkq40.js +1 -0
- package/dist/apps/playground/browser/chunk-Bej5a2xR.js +1 -0
- package/dist/apps/playground/browser/chunk-Bg3QC1eL.js +1 -0
- package/dist/apps/playground/browser/chunk-BsnTz1m5.js +1 -0
- package/dist/apps/playground/browser/chunk-C5kLunTi.js +1 -0
- package/dist/apps/playground/browser/chunk-C6tu3y81.js +1 -0
- package/dist/apps/playground/browser/chunk-C96WSpFp.js +1 -0
- package/dist/apps/playground/browser/chunk-CCnOVzbz.js +2 -0
- package/dist/apps/playground/browser/chunk-CERmZ9We.js +1 -0
- package/dist/apps/playground/browser/chunk-CZBTrY_H.js +68 -0
- package/dist/apps/playground/browser/chunk-Cg7brw6x.js +4070 -0
- package/dist/apps/playground/browser/chunk-CjTyStrr.js +1 -0
- package/dist/apps/playground/browser/chunk-CjYlnYC7.js +1 -0
- package/dist/apps/playground/browser/chunk-ClSUoZp8.js +292 -0
- package/dist/apps/playground/browser/chunk-CsrxfMap.js +1 -0
- package/dist/apps/playground/browser/chunk-CtndMAe2.js +1 -0
- package/dist/apps/playground/browser/chunk-Cy-ZOZI8.js +1 -0
- package/dist/apps/playground/browser/chunk-D3-jrtyv.js +1 -0
- package/dist/apps/playground/browser/chunk-D3vgrlDF.js +1 -0
- package/dist/apps/playground/browser/chunk-D48aVVTv.js +1 -0
- package/dist/apps/playground/browser/chunk-D4V3eSCv.js +1 -0
- package/dist/apps/playground/browser/chunk-DK715t6D.js +1 -0
- package/dist/apps/playground/browser/chunk-DUkct7WP.js +1 -0
- package/dist/apps/playground/browser/chunk-Y9ccR-I6.js +1 -0
- package/dist/apps/playground/browser/chunk-lfUHaltX.js +1 -0
- package/dist/apps/playground/browser/favicon.ico +0 -0
- package/dist/apps/playground/browser/index.html +14 -0
- package/dist/apps/playground/browser/main-WEJC3BLT.js +148 -0
- package/dist/apps/playground/browser/styles-4FSELHYD.css +1 -0
- package/dist/apps/playground/prerendered-routes.json +3 -0
- package/package.json +31 -8
|
@@ -0,0 +1,4070 @@
|
|
|
1
|
+
import {o}from'./chunk-CZBTrY_H.js';import {T,bA as Eu,aO as pt,k as TP,bB as RP,C as Co$1,u as QI,I as IE,Z,g,h as br$1,l as tI,e as ei$1,M as MI,a as gc,L as LE,o as nI,b9 as lp,J as zf,Q as Qy,f as gp,P as PE,m as mI,t as tp,X as XE,i as Ic,G as GE,c as Zf,d as Qf,bC as gD,bD as hD,p as Gl,z as zl,bE as oy}from'./main-WEJC3BLT.js';var Nt=hD((pi,It)=>{"use strict";function yt(e){return e instanceof Map?e.clear=e.delete=e.set=function(){throw new Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=function(){throw new Error("set is read-only")}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach(t=>{let n=e[t],i=typeof n;(i==="object"||i==="function")&&!Object.isFrozen(n)&&yt(n);}),e}var be=class{constructor(t){t.data===void 0&&(t.data={}),this.data=t.data,this.isMatchIgnored=!1;}ignoreMatch(){this.isMatchIgnored=!0;}};function vt(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function q(e,...t){let n=Object.create(null);for(let i in e)n[i]=e[i];return t.forEach(function(i){for(let m in i)n[m]=i[m];}),n}var en="</span>",mt=e=>!!e.scope,tn=(e,{prefix:t})=>{if(e.startsWith("language:"))return e.replace("language:","language-");if(e.includes(".")){let n=e.split(".");return [`${t}${n.shift()}`,...n.map((i,m)=>`${i}${"_".repeat(m+1)}`)].join(" ")}return `${t}${e}`},Pe=class{constructor(t,n){this.buffer="",this.classPrefix=n.classPrefix,t.walk(this);}addText(t){this.buffer+=vt(t);}openNode(t){if(!mt(t))return;let n=tn(t.scope,{prefix:this.classPrefix});this.span(n);}closeNode(t){mt(t)&&(this.buffer+=en);}value(){return this.buffer}span(t){this.buffer+=`<span class="${t}">`;}},ut=(e={})=>{let t={children:[]};return Object.assign(t,e),t},Le=class e{constructor(){this.rootNode=ut(),this.stack=[this.rootNode];}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(t){this.top.children.push(t);}openNode(t){let n=ut({scope:t});this.add(n),this.stack.push(n);}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(t){return this.constructor._walk(t,this.rootNode)}static _walk(t,n){return typeof n=="string"?t.addText(n):n.children&&(t.openNode(n),n.children.forEach(i=>this._walk(t,i)),t.closeNode(n)),t}static _collapse(t){typeof t!="string"&&t.children&&(t.children.every(n=>typeof n=="string")?t.children=[t.children.join("")]:t.children.forEach(n=>{e._collapse(n);}));}},Fe=class extends Le{constructor(t){super(),this.options=t;}addText(t){t!==""&&this.add(t);}startScope(t){this.openNode(t);}endScope(){this.closeNode();}__addSublanguage(t,n){let i=t.root;n&&(i.scope=`language:${n}`),this.add(i);}toHTML(){return new Pe(this,this.options).value()}finalize(){return this.closeAllNodes(),!0}};function se(e){return e?typeof e=="string"?e:e.source:null}function St(e){return J("(?=",e,")")}function nn(e){return J("(?:",e,")*")}function rn(e){return J("(?:",e,")?")}function J(...e){return e.map(n=>se(n)).join("")}function on(e){let t=e[e.length-1];return typeof t=="object"&&t.constructor===Object?(e.splice(e.length-1,1),t):{}}function ve(...e){return "("+(on(e).capture?"":"?:")+e.map(i=>se(i)).join("|")+")"}function Ct(e){return new RegExp(e.toString()+"|").exec("").length-1}function an(e,t){let n=e&&e.exec(t);return n&&n.index===0}var sn=new RegExp(ve(/\[(?:[^\\\]]|\\.)*\]/,/\(\?<(?![=!])[^>]+>/,/\(\?'[^']+'/,/\(\??/,/\\([1-9][0-9]*)/,/\\./));function Ue(e,{joinWith:t}){let n=0;return e.map(i=>{n+=1;let m=n,g=se(i),a="";for(;g.length>0;){let o=sn.exec(g);if(!o){a+=g;break}a+=g.substring(0,o.index),g=g.substring(o.index+o[0].length),o[0][0]==="\\"&&o[1]?a+="\\"+String(Number(o[1])+m):(a+=o[0],(o[0]==="("||/^\(\?[<']/.test(o[0]))&&n++);}return a}).map(i=>`(${i})`).join(t)}var ln=/\b\B/,xt="[a-zA-Z]\\w*",je="[a-zA-Z_]\\w*",wt="\\b\\d+(\\.\\d+)?",Et="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",At="\\b(0b[01]+)",cn="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",pn=(e={})=>{let t=/^#![ ]*\//;return e.binary&&(e.begin=J(t,/.*\b/,e.binary,/\b.*/)),q({scope:"meta",begin:t,end:/$/,relevance:0,"on:begin":(n,i)=>{n.index!==0&&i.ignoreMatch();}},e)},le={begin:"\\\\[\\s\\S]",relevance:0},dn={scope:"string",begin:"'",end:"'",illegal:"\\n",contains:[le]},mn={scope:"string",begin:'"',end:'"',illegal:"\\n",contains:[le]},un={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},Se=function(e,t,n={}){let i=q({scope:"comment",begin:e,end:t,contains:[]},n);i.contains.push({scope:"doctag",begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0});let m=ve("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/);return i.contains.push({begin:J(/[ ]+/,"(",m,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i},fn=Se("//","$"),gn=Se("/\\*","\\*/"),hn=Se("#","$"),bn={scope:"number",begin:wt,relevance:0},yn={scope:"number",begin:Et,relevance:0},vn={scope:"number",begin:At,relevance:0},Sn={scope:"regexp",begin:/\/(?=[^/\n]*\/)/,end:/\/[gimuy]*/,contains:[le,{begin:/\[/,end:/\]/,relevance:0,contains:[le]}]},Cn={scope:"title",begin:xt,relevance:0},xn={scope:"title",begin:je,relevance:0},wn={begin:"\\.\\s*"+je,relevance:0},En=function(e){return Object.assign(e,{"on:begin":(t,n)=>{n.data._beginMatch=t[1];},"on:end":(t,n)=>{n.data._beginMatch!==t[1]&&n.ignoreMatch();}})},he=Object.freeze({__proto__:null,APOS_STRING_MODE:dn,BACKSLASH_ESCAPE:le,BINARY_NUMBER_MODE:vn,BINARY_NUMBER_RE:At,COMMENT:Se,C_BLOCK_COMMENT_MODE:gn,C_LINE_COMMENT_MODE:fn,C_NUMBER_MODE:yn,C_NUMBER_RE:Et,END_SAME_AS_BEGIN:En,HASH_COMMENT_MODE:hn,IDENT_RE:xt,MATCH_NOTHING_RE:ln,METHOD_GUARD:wn,NUMBER_MODE:bn,NUMBER_RE:wt,PHRASAL_WORDS_MODE:un,QUOTE_STRING_MODE:mn,REGEXP_MODE:Sn,RE_STARTERS_RE:cn,SHEBANG:pn,TITLE_MODE:Cn,UNDERSCORE_IDENT_RE:je,UNDERSCORE_TITLE_MODE:xn});function An(e,t){e.input[e.index-1]==="."&&t.ignoreMatch();}function kn(e,t){e.className!==void 0&&(e.scope=e.className,delete e.className);}function _n(e,t){t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",e.__beforeBegin=An,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,e.relevance===void 0&&(e.relevance=0));}function Tn(e,t){Array.isArray(e.illegal)&&(e.illegal=ve(...e.illegal));}function Mn(e,t){if(e.match){if(e.begin||e.end)throw new Error("begin & end are not supported with match");e.begin=e.match,delete e.match;}}function In(e,t){e.relevance===void 0&&(e.relevance=1);}var Nn=(e,t)=>{if(!e.beforeMatch)return;if(e.starts)throw new Error("beforeMatch cannot be used with starts");let n=Object.assign({},e);Object.keys(e).forEach(i=>{delete e[i];}),e.keywords=n.keywords,e.begin=J(n.beforeMatch,St(n.begin)),e.starts={relevance:0,contains:[Object.assign(n,{endsParent:!0})]},e.relevance=0,delete n.beforeMatch;},Rn=["of","and","for","in","not","or","if","then","parent","list","value"],On="keyword";function kt(e,t,n=On){let i=Object.create(null);return typeof e=="string"?m(n,e.split(" ")):Array.isArray(e)?m(n,e):Object.keys(e).forEach(function(g){Object.assign(i,kt(e[g],t,g));}),i;function m(g,a){t&&(a=a.map(o=>o.toLowerCase())),a.forEach(function(o){let c=o.split("|");i[c[0]]=[g,Dn(c[0],c[1])];});}}function Dn(e,t){return t?Number(t):Pn(e)?0:1}function Pn(e){return Rn.includes(e.toLowerCase())}var ft={},X=e=>{console.error(e);},gt=(e,...t)=>{console.log(`WARN: ${e}`,...t);},re=(e,t)=>{ft[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),ft[`${e}/${t}`]=!0);},ye=new Error;function _t(e,t,{key:n}){let i=0,m=e[n],g={},a={};for(let o=1;o<=t.length;o++)a[o+i]=m[o],g[o+i]=!0,i+=Ct(t[o-1]);e[n]=a,e[n]._emit=g,e[n]._multi=!0;}function Ln(e){if(Array.isArray(e.begin)){if(e.skip||e.excludeBegin||e.returnBegin)throw X("skip, excludeBegin, returnBegin not compatible with beginScope: {}"),ye;if(typeof e.beginScope!="object"||e.beginScope===null)throw X("beginScope must be object"),ye;_t(e,e.begin,{key:"beginScope"}),e.begin=Ue(e.begin,{joinWith:""});}}function Fn(e){if(Array.isArray(e.end)){if(e.skip||e.excludeEnd||e.returnEnd)throw X("skip, excludeEnd, returnEnd not compatible with endScope: {}"),ye;if(typeof e.endScope!="object"||e.endScope===null)throw X("endScope must be object"),ye;_t(e,e.end,{key:"endScope"}),e.end=Ue(e.end,{joinWith:""});}}function Bn(e){e.scope&&typeof e.scope=="object"&&e.scope!==null&&(e.beginScope=e.scope,delete e.scope);}function Un(e){Bn(e),typeof e.beginScope=="string"&&(e.beginScope={_wrap:e.beginScope}),typeof e.endScope=="string"&&(e.endScope={_wrap:e.endScope}),Ln(e),Fn(e);}function jn(e){function t(a,o){return new RegExp(se(a),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(o?"g":""))}class n{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0;}addRule(o,c){c.position=this.position++,this.matchIndexes[this.matchAt]=c,this.regexes.push([c,o]),this.matchAt+=Ct(o)+1;}compile(){this.regexes.length===0&&(this.exec=()=>null);let o=this.regexes.map(c=>c[1]);this.matcherRe=t(Ue(o,{joinWith:"|"}),!0),this.lastIndex=0;}exec(o){this.matcherRe.lastIndex=this.lastIndex;let c=this.matcherRe.exec(o);if(!c)return null;let b=c.findIndex((M,A)=>A>0&&M!==void 0),v=this.matchIndexes[b];return c.splice(0,b),Object.assign(c,v)}}class i{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0;}getMatcher(o){if(this.multiRegexes[o])return this.multiRegexes[o];let c=new n;return this.rules.slice(o).forEach(([b,v])=>c.addRule(b,v)),c.compile(),this.multiRegexes[o]=c,c}resumingScanAtSamePosition(){return this.regexIndex!==0}considerAll(){this.regexIndex=0;}addRule(o,c){this.rules.push([o,c]),c.type==="begin"&&this.count++;}exec(o){let c=this.getMatcher(this.regexIndex);c.lastIndex=this.lastIndex;let b=c.exec(o);if(this.resumingScanAtSamePosition()&&!(b&&b.index===this.lastIndex)){let v=this.getMatcher(0);v.lastIndex=this.lastIndex+1,b=v.exec(o);}return b&&(this.regexIndex+=b.position+1,this.regexIndex===this.count&&this.considerAll()),b}}function m(a){let o=new i;return a.contains.forEach(c=>o.addRule(c.begin,{rule:c,type:"begin"})),a.terminatorEnd&&o.addRule(a.terminatorEnd,{type:"end"}),a.illegal&&o.addRule(a.illegal,{type:"illegal"}),o}function g(a,o){let c=a;if(a.isCompiled)return c;[kn,Mn,Un,Nn].forEach(v=>v(a,o)),e.compilerExtensions.forEach(v=>v(a,o)),a.__beforeBegin=null,[_n,Tn,In].forEach(v=>v(a,o)),a.isCompiled=!0;let b=null;return typeof a.keywords=="object"&&a.keywords.$pattern&&(a.keywords=Object.assign({},a.keywords),b=a.keywords.$pattern,delete a.keywords.$pattern),b=b||/\w+/,a.keywords&&(a.keywords=kt(a.keywords,e.case_insensitive)),c.keywordPatternRe=t(b,!0),o&&(a.begin||(a.begin=/\B|\b/),c.beginRe=t(c.begin),!a.end&&!a.endsWithParent&&(a.end=/\B|\b/),a.end&&(c.endRe=t(c.end)),c.terminatorEnd=se(c.end)||"",a.endsWithParent&&o.terminatorEnd&&(c.terminatorEnd+=(a.end?"|":"")+o.terminatorEnd)),a.illegal&&(c.illegalRe=t(a.illegal)),a.contains||(a.contains=[]),a.contains=[].concat(...a.contains.map(function(v){return zn(v==="self"?a:v)})),a.contains.forEach(function(v){g(v,c);}),a.starts&&g(a.starts,o),c.matcher=m(c),c}if(e.compilerExtensions||(e.compilerExtensions=[]),e.contains&&e.contains.includes("self"))throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return e.classNameAliases=q(e.classNameAliases||{}),g(e)}function Tt(e){return e?e.endsWithParent||Tt(e.starts):!1}function zn(e){return e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map(function(t){return q(e,{variants:null},t)})),e.cachedVariants?e.cachedVariants:Tt(e)?q(e,{starts:e.starts?q(e.starts):null}):Object.isFrozen(e)?q(e):e}var Gn="11.12.0",Be=class extends Error{constructor(t,n){super(t),this.name="HTMLInjectionError",this.html=n;}},De=vt,ht=q,bt=Symbol("nomatch"),Vn=7,Mt=function(e){let t=Object.create(null),n=Object.create(null),i=[],m=!0,g="Could not find the language '{}', did you forget to load/include a language module?",a={disableAutodetect:!0,name:"Plain text",contains:[]},o={ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",cssSelector:"pre code",languages:null,__emitter:Fe};function c(r){return o.noHighlightRe.test(r)}function b(r){let p=r.className+" ";p+=r.parentNode?r.parentNode.className:"";let d=o.languageDetectRe.exec(p);if(d){let h=D(d[1]);return h||(gt(g.replace("{}",d[1])),gt("Falling back to no-highlight mode for this block.",r)),h?d[1]:"no-highlight"}return p.split(/\s+/).find(h=>c(h)||D(h))}function v(r,p,d){let h="",C="";typeof p=="object"?(h=r,d=p.ignoreIllegals,C=p.language):(re("10.7.0","highlight(lang, code, ...args) has been deprecated."),re("10.7.0",`Please use highlight(code, options) instead.
|
|
2
|
+
https://github.com/highlightjs/highlight.js/issues/2277`),C=r,h=p),d===void 0&&(d=!0);let w={code:h,language:C};te("before:highlight",w);let N=w.result?w.result:M(w.language,w.code,d);return N.code=w.code,te("after:highlight",N),N}function M(r,p,d,h){let C=Object.create(null);function w(s,l){return s.keywords[l]}function N(){if(!u.keywords){x.addText(S);return}let s=0;u.keywordPatternRe.lastIndex=0;let l=u.keywordPatternRe.exec(S),f="";for(;l;){f+=S.substring(s,l.index);let y=F.case_insensitive?l[0].toLowerCase():l[0],E=w(u,y);if(E){let[z,Zt]=E;if(x.addText(f),f="",C[y]=(C[y]||0)+1,C[y]<=Vn&&(ue+=Zt),z.startsWith("_"))f+=l[0];else {let Qt=F.classNameAliases[z]||z;L(l[0],Qt);}}else f+=l[0];s=u.keywordPatternRe.lastIndex,l=u.keywordPatternRe.exec(S);}f+=S.substring(s),x.addText(f);}function Y(){if(S==="")return;let s=null;if(typeof u.subLanguage=="string"){if(!t[u.subLanguage]){x.addText(S);return}s=M(u.subLanguage,S,!0,$e[u.subLanguage]),$e[u.subLanguage]=s._top;}else s=O(S,u.subLanguage.length?u.subLanguage:null);u.relevance>0&&(ue+=s.relevance),x.__addSublanguage(s._emitter,s.language);}function I(){u.subLanguage!=null?Y():N(),S="";}function L(s,l){s!==""&&(x.startScope(l),x.addText(s),x.endScope());}function Ve(s,l){let f=1,y=l.length-1;for(;f<=y;){if(!s._emit[f]){f++;continue}let E=F.classNameAliases[s[f]]||s[f],z=l[f];E?L(z,E):(S=z,N(),S=""),f++;}}function He(s,l){return s.scope&&typeof s.scope=="string"&&x.openNode(F.classNameAliases[s.scope]||s.scope),s.beginScope&&(s.beginScope._wrap?(L(S,F.classNameAliases[s.beginScope._wrap]||s.beginScope._wrap),S=""):s.beginScope._multi&&(Ve(s.beginScope,l),S="")),u=Object.create(s,{parent:{value:u}}),u}function We(s,l,f){let y=an(s.endRe,f);if(y){if(s["on:end"]){let E=new be(s);s["on:end"](l,E),E.isMatchIgnored&&(y=!1);}if(y){for(;s.endsParent&&s.parent;)s=s.parent;return s}}if(s.endsWithParent)return We(s.parent,l,f)}function Wt(s){return u.matcher.regexIndex===0?(S+=s[0],1):(Me=!0,0)}function qt(s){let l=s[0],f=s.rule,y=new be(f),E=[f.__beforeBegin,f["on:begin"]];for(let z of E)if(z&&(z(s,y),y.isMatchIgnored))return Wt(l);return f.skip?S+=l:(f.excludeBegin&&(S+=l),I(),!f.returnBegin&&!f.excludeBegin&&(S=l)),He(f,s),f.returnBegin?0:l.length}function $t(s){let l=s[0],f=p.substring(s.index),y=We(u,s,f);if(!y)return bt;let E=u;u.endScope&&u.endScope._wrap?(I(),L(l,u.endScope._wrap)):u.endScope&&u.endScope._multi?(I(),Ve(u.endScope,s)):E.skip?S+=l:(E.returnEnd||E.excludeEnd||(S+=l),I(),E.excludeEnd&&(S=l));do u.scope&&x.closeNode(),!u.skip&&!u.subLanguage&&(ue+=u.relevance),u=u.parent;while(u!==y.parent);return y.starts&&He(y.starts,s),E.returnEnd?0:l.length}function Kt(){let s=[];for(let l=u;l!==F;l=l.parent)l.scope&&s.unshift(l.scope);s.forEach(l=>x.openNode(l));}let me={};function qe(s,l){let f=l&&l[0];if(S+=s,f==null)return I(),0;if(me.type==="begin"&&l.type==="end"&&me.index===l.index&&f===""){if(S+=p.slice(l.index,l.index+1),!m){let y=new Error(`0 width match regex (${r})`);throw y.languageName=r,y.badRule=me.rule,y}return 1}if(me=l,l.type==="begin")return qt(l);if(l.type==="illegal"&&!d){let y=new Error('Illegal lexeme "'+f+'" for mode "'+(u.scope||"<unnamed>")+'"');throw y.mode=u,y}else if(l.type==="end"){let y=$t(l);if(y!==bt)return y}if(l.type==="illegal"&&f==="")return l.index===p.length||(S+=`
|
|
3
|
+
`),1;if(Te>1e5&&Te>l.index*3)throw new Error("potential infinite loop, way more iterations than matches");return S+=f,f.length}let F=D(r);if(!F)throw X(g.replace("{}",r)),new Error('Unknown language: "'+r+'"');let Yt=jn(F),_e="",u=h||Yt,$e={},x=new o.__emitter(o);Kt();let S="",ue=0,Z=0,Te=0,Me=!1;try{if(F.__emitTokens)F.__emitTokens(p,x);else {for(u.matcher.considerAll();;){Te++,Me?Me=!1:u.matcher.considerAll(),u.matcher.lastIndex=Z;let s=u.matcher.exec(p);if(!s)break;let l=p.substring(Z,s.index),f=qe(l,s);Z=s.index+f;}qe(p.substring(Z));}return x.finalize(),_e=x.toHTML(),{language:r,value:_e,relevance:ue,illegal:!1,_emitter:x,_top:u}}catch(s){if(s.message&&s.message.includes("Illegal"))return {language:r,value:De(p),illegal:!0,relevance:0,_illegalBy:{message:s.message,index:Z,context:p.slice(Z-100,Z+100),mode:s.mode,resultSoFar:_e},_emitter:x};if(m)return {language:r,value:De(p),illegal:!1,relevance:0,errorRaised:s,_emitter:x,_top:u};throw s}}function A(r){let p={value:De(r),illegal:!1,relevance:0,_top:a,_emitter:new o.__emitter(o)};return p._emitter.addText(r),p}function O(r,p){p=p||o.languages||Object.keys(t);let d=A(r),h=p.filter(D).filter(de).map(I=>M(I,r,!1));h.unshift(d);let C=h.sort((I,L)=>{if(I.relevance!==L.relevance)return L.relevance-I.relevance;if(I.language&&L.language){if(D(I.language).supersetOf===L.language)return 1;if(D(L.language).supersetOf===I.language)return -1}return 0}),[w,N]=C,Y=w;return Y.secondBest=N,Y}function G(r,p,d){let h=p&&n[p]||d;r.classList.add("hljs"),r.classList.add(`language-${h}`);}function k(r){let p=null,d=b(r);if(c(d))return;if(te("before:highlightElement",{el:r,language:d}),r.dataset.highlighted){console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",r);return}if(r.children.length>0&&(o.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),console.warn("https://github.com/highlightjs/highlight.js/wiki/security"),console.warn("The element with unescaped HTML:"),console.warn(r)),o.throwUnescapedHTML))throw new Be("One of your code blocks includes unescaped HTML.",r.innerHTML);p=r;let h=p.textContent,C=d?v(h,{language:d,ignoreIllegals:!0}):O(h);r.innerHTML=C.value,r.dataset.highlighted="yes",G(r,d,C.language),r.result={language:C.language,re:C.relevance,relevance:C.relevance},C.secondBest&&(r.secondBest={language:C.secondBest.language,relevance:C.secondBest.relevance}),te("after:highlightElement",{el:r,result:C,text:h});}function V(r){o=ht(o,r);}let ee=()=>{j(),re("10.6.0","initHighlighting() deprecated. Use highlightAll() now.");};function P(){j(),re("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.");}let U=!1;function j(){function r(){j();}if(document.readyState==="loading"){U||window.addEventListener("DOMContentLoaded",r,!1),U=!0;return}document.querySelectorAll(o.cssSelector).forEach(k);}function $(r,p){let d=null;try{d=p(e);}catch(h){if(X("Language definition for '{}' could not be registered.".replace("{}",r)),m)X(h);else throw h;d=a;}d.name||(d.name=r),t[r]=d,d.rawDefinition=p.bind(null,e),d.aliases&&pe(d.aliases,{languageName:r});}function K(r){delete t[r];for(let p of Object.keys(n))n[p]===r&&delete n[p];}function xe(){return Object.keys(t)}function D(r){return r=(r||"").toLowerCase(),t[r]||t[n[r]]}function pe(r,{languageName:p}){typeof r=="string"&&(r=[r]),r.forEach(d=>{n[d.toLowerCase()]=p;});}function de(r){let p=D(r);return p&&!p.disableAutodetect}function we(r){r["before:highlightBlock"]&&!r["before:highlightElement"]&&(r["before:highlightElement"]=p=>{r["before:highlightBlock"](Object.assign({block:p.el},p));}),r["after:highlightBlock"]&&!r["after:highlightElement"]&&(r["after:highlightElement"]=p=>{r["after:highlightBlock"](Object.assign({block:p.el},p));});}function Ee(r){we(r),i.push(r);}function Ae(r){let p=i.indexOf(r);p!==-1&&i.splice(p,1);}function te(r,p){let d=r;i.forEach(function(h){h[d]&&h[d](p);});}function ke(r){return re("10.7.0","highlightBlock will be removed entirely in v12.0"),re("10.7.0","Please use highlightElement now."),k(r)}Object.assign(e,{highlight:v,highlightAuto:O,highlightAll:j,highlightElement:k,highlightBlock:ke,configure:V,initHighlighting:ee,initHighlightingOnLoad:P,registerLanguage:$,unregisterLanguage:K,listLanguages:xe,getLanguage:D,registerAliases:pe,autoDetection:de,inherit:ht,addPlugin:Ee,removePlugin:Ae}),e.debugMode=function(){m=!1;},e.safeMode=function(){m=!0;},e.versionString=Gn,e.regex={concat:J,lookahead:St,either:ve,optional:rn,anyNumberOfTimes:nn};for(let r in he)typeof he[r]=="object"&&yt(he[r]);return Object.assign(e,he),e},oe=Mt({});oe.newInstance=()=>Mt({});It.exports=oe;oe.HighlightJS=oe;oe.default=oe;});var Rt=gD(Nt());var R=Rt.default;function ze(e){let t=e.regex,n={},i={begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[n]}]};Object.assign(n,{className:"variable",variants:[{begin:t.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},i]});let m={className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},g=e.inherit(e.COMMENT(),{match:[/(^|\s)/,/#.*$/],scope:{2:"comment"}}),a={begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,className:"string"})]}},o={className:"string",begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,n,m]};m.contains.push(o);let c={match:/\\"/},b={className:"string",begin:/'/,end:/'/},v={match:/\\'/},M={begin:/\$?\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,n]},A=["fish","bash","zsh","sh","csh","ksh","tcsh","dash","scsh"],O=e.SHEBANG({binary:`(${A.join("|")})`,relevance:10}),G={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:true,contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0},k=["if","then","else","elif","fi","time","for","while","until","in","do","done","case","esac","coproc","function","select"],V=["true","false"],ee={match:/(\/[a-z._-]+)+/},P=["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset"],U=["alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","sudo","type","typeset","ulimit","unalias"],j=["autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp"],$=["chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"];return {name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/,keyword:k,literal:V,built_in:[...P,...U,"set","shopt",...j,...$]},contains:[O,e.SHEBANG(),G,M,g,a,ee,o,c,b,v,n]}}var Hn=e=>({IMPORTANT:{scope:"meta",begin:"!important"},BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number",begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},UNICODE_RANGE:{scope:"number",begin:/\b[Uu]\+[0-9A-Fa-f][0-9A-Fa-f?]{0,5}(-[0-9A-Fa-f][0-9A-Fa-f]{0,5})?/},FUNCTION_DISPATCH:{className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$",contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{scope:"number",begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/}}),Wn=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","optgroup","option","p","picture","q","quote","samp","section","select","source","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],qn=["defs","g","marker","mask","pattern","svg","switch","symbol","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feFlood","feGaussianBlur","feImage","feMerge","feMorphology","feOffset","feSpecularLighting","feTile","feTurbulence","linearGradient","radialGradient","stop","circle","ellipse","image","line","path","polygon","polyline","rect","text","use","textPath","tspan","foreignObject","clipPath"],$n=[...Wn,...qn],Kn=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"].sort().reverse(),Yn=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"].sort().reverse(),Zn=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"].sort().reverse(),Qn=["accent-color","align-content","align-items","align-self","alignment-baseline","all","anchor-name","animation","animation-composition","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-range","animation-range-end","animation-range-start","animation-timeline","animation-timing-function","appearance","aspect-ratio","backdrop-filter","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-position-x","background-position-y","background-repeat","background-size","baseline-shift","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-end-end-radius","border-end-start-radius","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-start-end-radius","border-start-start-radius","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-align","box-decoration-break","box-direction","box-flex","box-flex-group","box-lines","box-ordinal-group","box-orient","box-pack","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","color-scheme","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","contain-intrinsic-block-size","contain-intrinsic-height","contain-intrinsic-inline-size","contain-intrinsic-size","contain-intrinsic-width","container","container-name","container-type","content","content-visibility","corner-bottom-left-shape","corner-bottom-right-shape","corner-shape","corner-top-left-shape","corner-top-right-shape","counter-increment","counter-reset","counter-set","cue","cue-after","cue-before","cursor","cx","cy","direction","display","dominant-baseline","empty-cells","enable-background","field-sizing","fill","fill-opacity","fill-rule","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flood-color","flood-opacity","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-optical-sizing","font-palette","font-size","font-size-adjust","font-smooth","font-smoothing","font-stretch","font-style","font-synthesis","font-synthesis-position","font-synthesis-small-caps","font-synthesis-style","font-synthesis-weight","font-variant","font-variant-alternates","font-variant-caps","font-variant-east-asian","font-variant-emoji","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","forced-color-adjust","gap","glyph-orientation-horizontal","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphenate-character","hyphenate-limit-chars","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","initial-letter","initial-letter-align","inline-size","inset","inset-area","inset-block","inset-block-end","inset-block-start","inset-inline","inset-inline-end","inset-inline-start","isolation","justify-content","justify-items","justify-self","kerning","left","letter-spacing","lighting-color","line-break","line-height","line-height-step","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","margin-trim","marker","marker-end","marker-mid","marker-start","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","masonry-auto-flow","math-depth","math-shift","math-style","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","offset","offset-anchor","offset-distance","offset-path","offset-position","offset-rotate","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-anchor","overflow-block","overflow-clip-margin","overflow-inline","overflow-wrap","overflow-x","overflow-y","overlay","overscroll-behavior","overscroll-behavior-block","overscroll-behavior-inline","overscroll-behavior-x","overscroll-behavior-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page","page-break-after","page-break-before","page-break-inside","paint-order","pause","pause-after","pause-before","perspective","perspective-origin","place-content","place-items","place-self","pointer-events","position","position-anchor","position-visibility","print-color-adjust","quotes","r","resize","rest","rest-after","rest-before","right","rotate","row-gap","ruby-align","ruby-position","scale","scroll-behavior","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scroll-timeline","scroll-timeline-axis","scroll-timeline-name","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","shape-rendering","speak","speak-as","src","stop-color","stop-opacity","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","tab-size","table-layout","text-align","text-align-all","text-align-last","text-anchor","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-skip","text-decoration-skip-ink","text-decoration-style","text-decoration-thickness","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-size-adjust","text-transform","text-underline-offset","text-underline-position","text-wrap","text-wrap-mode","text-wrap-style","timeline-scope","top","touch-action","transform","transform-box","transform-origin","transform-style","transition","transition-behavior","transition-delay","transition-duration","transition-property","transition-timing-function","translate","unicode-bidi","unicode-range","user-modify","user-select","vector-effect","vertical-align","view-timeline","view-timeline-axis","view-timeline-inset","view-timeline-name","view-transition-name","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","white-space-collapse","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","x","y","z-index","zoom"].sort().reverse();function Ot(e){let t=e.regex,n=Hn(e),i={begin:/-(webkit|moz|ms|o)-(?=[a-z])/},m="and or not only",g=/@-?\w[\w]*(-\w+)*/,a="[a-zA-Z-][a-zA-Z0-9_-]*",o=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE];return {name:"CSS",case_insensitive:true,illegal:/[=|'\$]/,keywords:{keyframePosition:"from to"},classNameAliases:{keyframePosition:"selector-tag"},contains:[n.BLOCK_COMMENT,i,n.CSS_NUMBER_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0},{className:"selector-class",begin:"\\."+a,relevance:0},n.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{begin:":("+Yn.join("|")+")"},{begin:":(:)?("+Zn.join("|")+")"}]},n.CSS_VARIABLE,{className:"attribute",begin:"\\b("+Qn.join("|")+")\\b"},{begin:/:/,end:/[;}{]/,contains:[n.BLOCK_COMMENT,n.HEXCOLOR,n.IMPORTANT,n.CSS_NUMBER_MODE,n.UNICODE_RANGE,...o,{begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri"},contains:[...o,{className:"string",begin:/[^)]/,endsWithParent:true,excludeEnd:true}]},n.FUNCTION_DISPATCH]},{begin:t.lookahead(/@/),end:"[{;]",relevance:0,illegal:/:/,contains:[{className:"keyword",begin:g},{begin:/\s/,endsWithParent:true,excludeEnd:true,relevance:0,keywords:{$pattern:/[a-z-]+/,keyword:m,attribute:Kn.join(" ")},contains:[{begin:/[a-z-]+(?=:)/,className:"attribute"},...o,n.CSS_NUMBER_MODE]}]},{className:"selector-tag",begin:"\\b("+$n.join("|")+")\\b"}]}}var Xn="([-+]?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)|NaN|[-+]?Infinity",Jn={scope:"number",match:Xn,relevance:0};function Dt(e){let t={className:"attr",begin:/(("(\\.|[^\\"\r\n])*")|('(\\.|[^\\'\r\n])*'))(?=\s*:)/,relevance:1.01},n={match:/[{}[\],:]/,className:"punctuation",relevance:0},i=["true","false","null"],m={scope:"literal",beginKeywords:i.join(" ")};return {name:"JSON",aliases:["jsonc","json5"],keywords:{literal:i},contains:[t,n,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,m,Jn,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE],illegal:"\\S"}}var Ce="[A-Za-z$_][0-9A-Za-z$_]*",Pt=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends","using"],Lt=["true","false","null","undefined","NaN","Infinity"],Ft=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],Bt=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],Ut=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],jt=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","self","global"],zt=[].concat(Ut,Ft,Bt);function ei(e){let t=e.regex,n=(d,{after:h})=>{let C="</"+d[0].slice(1);return d.input.indexOf(C,h)!==-1},i=Ce,m={begin:"<>",end:"</>"},g=/<[A-Za-z0-9\\._:-]+\s*\/>/,a={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(d,h)=>{let C=d[0].length+d.index,w=d.input[C];if(w==="<"||w===","){h.ignoreMatch();return}w===">"&&(n(d,{after:C})||h.ignoreMatch());let N,Y=d.input.substring(C);if(N=Y.match(/^\s*=/)){h.ignoreMatch();return}if((N=Y.match(/^\s+extends\s+/))&&N.index===0){h.ignoreMatch();return}}},o={$pattern:Ce,keyword:Pt,literal:Lt,built_in:zt,"variable.language":jt},c="[0-9](_?[0-9])*",b=`\\.(${c})`,v="0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*",M={className:"number",variants:[{begin:`(\\b(${v})((${b})|\\.)?|(${b}))[eE][+-]?(${c})\\b`},{begin:`\\b(${v})\\b((${b})\\b|\\.)?|(${b})\\b`},{begin:"\\b(0|[1-9](_?[0-9])*)n\\b"},{begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*n?\\b"},{begin:"\\b0[oO][0-7](_?[0-7])*n?\\b"},{begin:"\\b0[0-7]+n?\\b"}],relevance:0},A={className:"subst",begin:"\\$\\{",end:"\\}",keywords:o,contains:[]},O={begin:".?html`",end:"",starts:{end:"`",returnEnd:false,contains:[e.BACKSLASH_ESCAPE,A],subLanguage:"xml"}},G={begin:".?css`",end:"",starts:{end:"`",returnEnd:false,contains:[e.BACKSLASH_ESCAPE,A],subLanguage:"css"}},k={begin:".?gql`",end:"",starts:{end:"`",returnEnd:false,contains:[e.BACKSLASH_ESCAPE,A],subLanguage:"graphql"}},V={className:"string",begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE,A]},P={className:"comment",variants:[e.COMMENT(/\/\*\*(?!\/)/,"\\*/",{relevance:0,contains:[{begin:"(?=@[A-Za-z]+)",relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+"},{className:"type",begin:"\\{",end:"\\}",excludeEnd:true,excludeBegin:true,relevance:0},{className:"variable",begin:i+"(?=\\s*(-)|$)",endsParent:true,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]}),e.C_BLOCK_COMMENT_MODE,e.C_LINE_COMMENT_MODE]},U=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,O,G,k,V,{match:/\$\d+/},M];A.contains=U.concat({begin:/\{/,end:/\}/,keywords:o,contains:["self"].concat(U)});let j=[].concat(P,A.contains),$=j.concat([{begin:/(\s*)\(/,end:/\)/,keywords:o,contains:["self"].concat(j)}]),K={className:"params",begin:/(\s*)\(/,end:/\)/,excludeBegin:true,excludeEnd:true,keywords:o,contains:$},xe={variants:[{match:[/class/,/\s+/,i,/\s+/,/extends/,/\s+/,t.concat(i,"(",t.concat(/\./,i),")*")],scope:{1:"keyword",3:"title.class",5:"keyword",7:"title.class.inherited"}},{match:[/class/,/\s+/,i],scope:{1:"keyword",3:"title.class"}}]},D={relevance:0,match:t.either(/\bJSON/,/\b[A-Z][a-z]+([A-Z][a-z]*|\d)*/,/\b[A-Z]{2,}([A-Z][a-z]+|\d)+([A-Z][a-z]*)*/,/\b[A-Z]{2,}[a-z]+([A-Z][a-z]+|\d)*([A-Z][a-z]*)*/),className:"title.class",keywords:{_:[...Ft,...Bt]}},pe={label:"use_strict",className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},de={variants:[{match:[/function/,/\s+/,i,/(?=\s*\()/]},{match:[/function/,/\s*(?=\()/]}],className:{1:"keyword",3:"title.function"},label:"func.def",contains:[K],illegal:/%/},we={relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/,className:"variable.constant"};function Ee(d){return t.concat("(?!",d.join("|"),")")}let Ae={match:t.concat(/\b/,Ee([...Ut,"super","import","await"].map(d=>`${d}\\s*\\(`)),i,t.lookahead(/\s*\(/)),className:"title.function",relevance:0},te={begin:t.concat(/\./,t.lookahead(t.concat(i,/(?![0-9A-Za-z$_(])/))),end:i,excludeBegin:true,keywords:"prototype",className:"property",relevance:0},ke={match:[/get|set/,/\s+/,i,/(?=\()/],className:{1:"keyword",3:"title.function"},contains:[{begin:/\(\)/},K]},r="(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+e.UNDERSCORE_IDENT_RE+")\\s*=>",p={match:[/const|var|let/,/\s+/,i,/\s*/,/=\s*/,/(async\s*)?/,t.lookahead(r)],keywords:"async",className:{1:"keyword",3:"title.function"},contains:[K]};return {name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:o,exports:{PARAMS_CONTAINS:$,CLASS_REFERENCE:D},illegal:/#(?![$_A-Za-z])/,contains:[e.SHEBANG({label:"shebang",binary:"node",relevance:5}),pe,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,O,G,k,V,P,{match:/\$\d+/},M,D,{scope:"attr",match:i+t.lookahead(":"),relevance:0},p,{begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",relevance:0,contains:[P,e.REGEXP_MODE,{className:"function",begin:r,returnBegin:true,end:"\\s*=>",contains:[{className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:true},{begin:/(\s*)\(/,end:/\)/,excludeBegin:true,excludeEnd:true,keywords:o,contains:$}]}]},{begin:/,/,relevance:0},{match:/\s+/,relevance:0},{variants:[{begin:m.begin,end:m.end},{match:g},{begin:a.begin,"on:begin":a.isTrulyOpeningTag,end:a.end}],subLanguage:"xml",contains:[{begin:a.begin,end:a.end,skip:true,contains:["self"]}]}]},de,{beginKeywords:"while if switch catch for"},{begin:"\\b(?!function)"+e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",returnBegin:true,label:"func.def",contains:[K,e.inherit(e.TITLE_MODE,{begin:i,className:"title.function"})]},{match:/\.\.\./,relevance:0},te,{match:"\\$"+i,relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"},contains:[K]},Ae,we,xe,ke,{match:/\$[(.]/}]}}function ce(e){let t=e.regex,n=ei(e),i=Ce,m=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],g={begin:[/namespace/,/\s+/,e.IDENT_RE],beginScope:{1:"keyword",3:"title.class"}},a={beginKeywords:"interface",end:/\{/,excludeEnd:true,keywords:{keyword:"interface extends",built_in:m},contains:[n.exports.CLASS_REFERENCE]},o={className:"meta",relevance:10,begin:/^\s*['"]use strict['"]/},c=["type","interface","public","private","protected","implements","declare","abstract","readonly","enum","override","satisfies"],b={$pattern:Ce,keyword:Pt.concat(c),literal:Lt,built_in:zt.concat(m),"variable.language":jt},v={className:"meta",begin:"@"+i},M=(k,V,ee)=>{let P=k.contains.findIndex(U=>U.label===V);if(P===-1)throw new Error("can not find mode to replace");k.contains.splice(P,1,ee);};Object.assign(n.keywords,b),n.exports.PARAMS_CONTAINS.push(v);let A=n.contains.find(k=>k.scope==="attr"),O=Object.assign({},A,{match:t.concat(i,t.lookahead(/\s*\?:/))});n.exports.PARAMS_CONTAINS.push([n.exports.CLASS_REFERENCE,A,O]),n.contains=n.contains.concat([v,g,a,O]),M(n,"shebang",e.SHEBANG()),M(n,"use_strict",o);let G=n.contains.find(k=>k.label==="func.def");return G.relevance=0,Object.assign(n,{name:"TypeScript",aliases:["ts","tsx","mts","cts"]}),n}function Ge(e){let t=e.regex,n=t.concat(/[\p{L}_]/u,t.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),i=/[\p{L}0-9._:-]+/u,m={className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},g={begin:/\s/,contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]},a=e.inherit(g,{begin:/\(/,end:/\)/}),o=e.inherit(e.APOS_STRING_MODE,{className:"string"}),c=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),b={endsWithParent:true,illegal:/</,relevance:0,contains:[{className:"attr",begin:i,relevance:0},{begin:/=\s*/,relevance:0,contains:[{className:"string",endsParent:true,variants:[{begin:/"/,end:/"/,contains:[m]},{begin:/'/,end:/'/,contains:[m]},{begin:/[^\s"'=<>`]+/}]}]}]};return {name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:true,unicodeRegex:true,contains:[{className:"meta",begin:/<![a-z]/,end:/>/,relevance:10,contains:[g,c,o,a,{begin:/\[/,end:/\]/,contains:[{className:"meta",begin:/<![a-z]/,end:/>/,contains:[g,a,c,o]}]}]},e.COMMENT(/<!--/,/-->/,{relevance:10}),{begin:/<!\[CDATA\[/,end:/\]\]>/,relevance:10},m,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/,relevance:10,contains:[c]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag",begin:/<style(?=\s|>)/,end:/>/,keywords:{name:"style"},contains:[b],starts:{end:/<\/style>/,returnEnd:true,subLanguage:"css"}},{className:"tag",begin:/<script(?=\s|>)/,end:/>/,keywords:{name:"script"},contains:[b],starts:{end:/<\/script>/,returnEnd:true,subLanguage:"javascript"}},{className:"tag",begin:/<>|<\/>/},{className:"tag",begin:t.concat(/</,t.lookahead(t.concat(n,t.either(/\/>/,/>/,/\s/)))),end:/\/?>/,contains:[{className:"name",begin:n,relevance:0,starts:b}]},{className:"tag",begin:t.concat(/<\//,t.lookahead(t.concat(n,/>/))),contains:[{className:"name",begin:n,relevance:0},{begin:/>/,relevance:0,endsParent:true}]}]}}R.registerLanguage("typescript",ce);R.registerLanguage("ts",ce);R.registerLanguage("javascript",ce);R.registerLanguage("js",ce);R.registerLanguage("xml",Ge);R.registerLanguage("html",Ge);R.registerLanguage("css",Ot);R.registerLanguage("bash",ze);R.registerLanguage("shell",ze);R.registerLanguage("json",Dt);function Gt(e,t="typescript"){try{return R.highlight(e,{language:t,ignoreIllegals:!0}).value}catch{return ti(e)}}function ti(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function ni(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function ii(e){return e.split(/(<[^>]+>)/g).map(t=>{if(t.startsWith("<"))return t;let n=t;for(let{id:i,match:m}of o){let g=new RegExp(`\\b${ni(m)}\\b`,"g");n=n.replace(g,`<a href="/lecture#${i}" class="pg-glossary-link">${m}</a>`);}return n}).join("")}function Vt(e,t="typescript"){return ii(Gt(e,t))}var ri=["*"];function oi(e,t){if(e&1&&(ei$1(0,"span",5),MI(1),gc()),e&2){let n=XE();Qy(),Ic(" ",n.badge()," ");}}function ai(e,t){if(e&1&&(ei$1(0,"p",6),MI(1),gc()),e&2){let n=XE();Qy(),Ic(" ",n.hint()," ");}}function si(e,t){if(e&1){let n=GE();ei$1(0,"button",10),tp("click",function(){Gl(n);let m=XE();return zl(m.toggleCode())}),Zf(1,"africanies-icon",11),MI(2),gc();}if(e&2){let n=XE();zf("aria-expanded",n.codeOpen()),Qy(),Qf("name",n.codeOpen()?"close":"code")("size",14),Qy(),Ic(" ",n.codeOpen()?"Hide code":"Show code"," ");}}function li(e,t){if(e&1&&(ei$1(0,"p",8),MI(1),gc()),e&2){let n=XE();Qy(),Ic(" ",n.subtext()," ");}}function ci(e,t){if(e&1&&(ei$1(0,"div",9)(1,"div",12)(2,"span",13),MI(3," Implementation "),ei$1(4,"span",14),MI(5,"TypeScript"),gc()(),Zf(6,"africanies-copy",15),gc(),ei$1(7,"pre",16),Zf(8,"code",17),gc()()),e&2){let n=XE();Qy(6),Qf("value",n.code()),Qy(2),Qf("innerHTML",n.highlightedCode(),oy);}}var Ht=class e{sanitizer=T(Eu);router=T(pt);title=TP.required();anchorId=TP(null);hint=TP(null);subtext=TP(null);badge=TP(null);muted=TP(false,{transform:RP});code=TP(null);codeOpen=Co$1(false);hasCode=QI(()=>{let t=this.code();return t!=null&&t.trim().length>0});highlightedCode=QI(()=>{let t=this.code()??"",n=Vt(t,"typescript");return this.sanitizer.bypassSecurityTrustHtml(n)});toggleCode(){this.codeOpen.update(t=>!t);}onGlossaryLinkClick(t){let n=t.target.closest("a.pg-glossary-link");if(!(n instanceof HTMLAnchorElement))return;t.preventDefault();let i=new URL(n.href,window.location.origin);this.router.navigate([i.pathname],{fragment:i.hash.replace(/^#/,"")||void 0});}static \u0275fac=function(n){return new(n||e)};static \u0275cmp=IE({type:e,selectors:[["app-demo-section"]],hostBindings:function(n,i){n&1&&tp("click",function(g){return i.onGlossaryLinkClick(g)});},inputs:{title:[1,"title"],anchorId:[1,"anchorId"],hint:[1,"hint"],subtext:[1,"subtext"],badge:[1,"badge"],muted:[1,"muted"],code:[1,"code"]},ngContentSelectors:ri,decls:13,vars:11,consts:[[1,"flex","flex-col","gap-4"],[1,"flex","flex-col","gap-2","sm:flex-row","sm:items-start","sm:justify-between"],[1,"flex","min-w-0","flex-col","gap-1"],[1,"flex","flex-wrap","items-center","gap-2"],[1,"m-0","text-heading-3","text-ink","dark:text-white"],[1,"rounded-md","bg-border","px-2","py-0.5","text-caption","text-neutral-600","dark:bg-white/10","dark:text-neutral-400"],[1,"m-0","text-body-sm","text-neutral-600","dark:text-neutral-400"],["africanies-button","","type","button","variant","ghost","size","sm",1,"shrink-0","self-start"],[1,"m-0","text-caption","text-neutral-500","dark:text-neutral-400"],[1,"overflow-hidden","rounded-xl","border","border-border","bg-[#1e1e1e]","text-white","dark:border-white/10"],["africanies-button","","type","button","variant","ghost","size","sm",1,"shrink-0","self-start",3,"click"],[3,"name","size"],[1,"flex","items-center","justify-between","gap-2","border-b","border-white/10","px-4","py-2"],[1,"text-caption","font-medium","uppercase","tracking-wide","text-white/50"],[1,"ml-2","normal-case","tracking-normal","text-white/35"],["label","Copy","buttonClass","!text-white/80 hover:!bg-white/10 hover:!text-white",3,"value"],[1,"pg-code-block","m-0","max-h-[min(70vh,40rem)]","overflow-auto","p-4","font-mono","text-caption","leading-relaxed"],[1,"hljs","language-typescript",3,"innerHTML"]],template:function(n,i){n&1&&(tI(),ei$1(0,"section",0)(1,"div",1)(2,"div",2)(3,"div",3)(4,"h2",4),MI(5),gc(),LE(6,oi,2,1,"span",5),gc(),LE(7,ai,2,1,"p",6),gc(),LE(8,si,3,4,"button",7),gc(),ei$1(9,"div"),nI(10),gc(),LE(11,li,2,1,"p",8),LE(12,ci,9,2,"div",9),gc()),n&2&&(lp("scroll-mt-24",i.anchorId()!==null),zf("id",i.anchorId()||null),Qy(5),gp(i.title()),Qy(),PE(i.badge()?6:-1),Qy(),PE(i.hint()?7:-1),Qy(),PE(i.hasCode()?8:-1),Qy(),mI(i.muted()?"pg-demo-muted":"pg-demo"),Qy(2),PE(i.subtext()?11:-1),Qy(),PE(i.hasCode()&&i.codeOpen()?12:-1));},dependencies:[Z,g,br$1],encapsulation:2})};var Bi=`// Default ellipsis trigger \u2014 good for table rows.
|
|
4
|
+
// Put side effects on each item's onClick; the menu closes after it runs.
|
|
5
|
+
// Panel renders in a CDK overlay so it won't get clipped by overflow:hidden.
|
|
6
|
+
|
|
7
|
+
import { Component, signal } from '@angular/core';
|
|
8
|
+
import { ActionMenuComponent, type AfricaniesMenuItem } from '@africanies/africanies-ui';
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
selector: 'app-shipment-row-actions-demo',
|
|
12
|
+
standalone: true,
|
|
13
|
+
imports: [ActionMenuComponent],
|
|
14
|
+
template: \`
|
|
15
|
+
<div class="flex flex-wrap items-center gap-4">
|
|
16
|
+
<africanies-action-menu [items]="items" ariaLabel="Shipment actions" />
|
|
17
|
+
|
|
18
|
+
<p class="m-0 text-body-sm">
|
|
19
|
+
Last action:
|
|
20
|
+
<span class="font-medium">{{ lastAction() ?? '\u2014' }}</span>
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
\`,
|
|
24
|
+
})
|
|
25
|
+
export class ShipmentRowActionsDemoComponent {
|
|
26
|
+
protected readonly lastAction = signal<string | null>(null);
|
|
27
|
+
|
|
28
|
+
protected readonly items: AfricaniesMenuItem[] = [
|
|
29
|
+
{
|
|
30
|
+
label: 'Open',
|
|
31
|
+
icon: 'eye',
|
|
32
|
+
onClick: () => this.lastAction.set('Open'),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: 'Edit',
|
|
36
|
+
icon: 'edit',
|
|
37
|
+
onClick: () => this.lastAction.set('Edit'),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: 'Copy reference',
|
|
41
|
+
icon: 'copy',
|
|
42
|
+
onClick: () => this.lastAction.set('Copy reference'),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
label: 'Delete',
|
|
46
|
+
icon: 'trash',
|
|
47
|
+
danger: true,
|
|
48
|
+
dividerBefore: true,
|
|
49
|
+
onClick: () => this.lastAction.set('Delete'),
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
`,Ui=`// Swap the ellipsis for your own trigger \u2014 project it inside and add africaniesActionMenuTrigger.
|
|
54
|
+
|
|
55
|
+
import { Component } from '@angular/core';
|
|
56
|
+
import {
|
|
57
|
+
ActionMenuComponent,
|
|
58
|
+
ActionMenuTriggerDirective,
|
|
59
|
+
ButtonComponent,
|
|
60
|
+
type AfricaniesMenuItem,
|
|
61
|
+
} from '@africanies/africanies-ui';
|
|
62
|
+
|
|
63
|
+
@Component({
|
|
64
|
+
selector: 'app-shipment-toolbar-actions',
|
|
65
|
+
standalone: true,
|
|
66
|
+
imports: [ActionMenuComponent, ActionMenuTriggerDirective, ButtonComponent],
|
|
67
|
+
template: \`
|
|
68
|
+
<africanies-action-menu [items]="items" ariaLabel="Shipment actions">
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
africanies-button
|
|
72
|
+
africaniesActionMenuTrigger
|
|
73
|
+
variant="secondary"
|
|
74
|
+
size="sm"
|
|
75
|
+
>
|
|
76
|
+
Actions
|
|
77
|
+
</button>
|
|
78
|
+
</africanies-action-menu>
|
|
79
|
+
\`,
|
|
80
|
+
})
|
|
81
|
+
export class ShipmentToolbarActionsComponent {
|
|
82
|
+
protected readonly items: AfricaniesMenuItem[] = [
|
|
83
|
+
{
|
|
84
|
+
label: 'Open',
|
|
85
|
+
icon: 'eye',
|
|
86
|
+
onClick: () => this.open(),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
label: 'Edit',
|
|
90
|
+
icon: 'edit',
|
|
91
|
+
onClick: () => this.edit(),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
label: 'Delete',
|
|
95
|
+
icon: 'trash',
|
|
96
|
+
danger: true,
|
|
97
|
+
dividerBefore: true,
|
|
98
|
+
onClick: () => this.delete(),
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
private open(): void {
|
|
103
|
+
/* navigate or open drawer */
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private edit(): void {
|
|
107
|
+
/* \u2026 */
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private delete(): void {
|
|
111
|
+
/* \u2026 */
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
`,ji=`// Item flags: danger for destructive, disabled when unavailable, dividerBefore to separate groups.
|
|
115
|
+
|
|
116
|
+
import { Component } from '@angular/core';
|
|
117
|
+
import { ActionMenuComponent, type AfricaniesMenuItem } from '@africanies/africanies-ui';
|
|
118
|
+
|
|
119
|
+
@Component({
|
|
120
|
+
selector: 'app-shipment-action-variants',
|
|
121
|
+
standalone: true,
|
|
122
|
+
imports: [ActionMenuComponent],
|
|
123
|
+
template: \`
|
|
124
|
+
<africanies-action-menu [items]="items" />
|
|
125
|
+
\`,
|
|
126
|
+
})
|
|
127
|
+
export class ShipmentActionVariantsComponent {
|
|
128
|
+
protected readonly items: AfricaniesMenuItem[] = [
|
|
129
|
+
{
|
|
130
|
+
label: 'Download PDF',
|
|
131
|
+
icon: 'download',
|
|
132
|
+
onClick: () => this.download(),
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
label: 'Archive',
|
|
136
|
+
disabled: true,
|
|
137
|
+
onClick: () => undefined,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
label: 'Void shipment',
|
|
141
|
+
icon: 'trash',
|
|
142
|
+
danger: true,
|
|
143
|
+
dividerBefore: true,
|
|
144
|
+
onClick: () => this.voidShipment(),
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
private download(): void {
|
|
149
|
+
/* \u2026 */
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private voidShipment(): void {
|
|
153
|
+
/* \u2026 */
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
`,zi=`// [disabled] locks the whole menu (won't open). Prefer this over an empty items array.
|
|
157
|
+
|
|
158
|
+
import { Component, input } from '@angular/core';
|
|
159
|
+
import { ActionMenuComponent, type AfricaniesMenuItem } from '@africanies/africanies-ui';
|
|
160
|
+
|
|
161
|
+
@Component({
|
|
162
|
+
selector: 'app-shipment-action-menu-disabled',
|
|
163
|
+
standalone: true,
|
|
164
|
+
imports: [ActionMenuComponent],
|
|
165
|
+
template: \`
|
|
166
|
+
<africanies-action-menu [items]="items" [disabled]="menuDisabled()" />
|
|
167
|
+
\`,
|
|
168
|
+
})
|
|
169
|
+
export class ShipmentActionMenuDisabledComponent {
|
|
170
|
+
// Parent passes true when the row is locked or the user lacks edit rights.
|
|
171
|
+
readonly menuDisabled = input(false);
|
|
172
|
+
|
|
173
|
+
protected readonly items: AfricaniesMenuItem[] = [
|
|
174
|
+
{
|
|
175
|
+
label: 'Open',
|
|
176
|
+
icon: 'eye',
|
|
177
|
+
onClick: () => this.open(),
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
label: 'Edit',
|
|
181
|
+
icon: 'edit',
|
|
182
|
+
onClick: () => this.edit(),
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
label: 'Delete',
|
|
186
|
+
icon: 'trash',
|
|
187
|
+
danger: true,
|
|
188
|
+
dividerBefore: true,
|
|
189
|
+
onClick: () => this.delete(),
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
private open(): void {
|
|
194
|
+
/* not reachable while [disabled]="true" */
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private edit(): void {
|
|
198
|
+
/* \u2026 */
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private delete(): void {
|
|
202
|
+
/* \u2026 */
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
`;var Vi=`
|
|
206
|
+
// Inline banners for info / success / warning / danger.
|
|
207
|
+
// Parent owns visibility. Not a toast, and not for full-page fetch failures.
|
|
208
|
+
|
|
209
|
+
import { Component, signal } from '@angular/core';
|
|
210
|
+
import { AlertComponent, ButtonComponent, type AlertVariant } from '@africanies/africanies-ui';
|
|
211
|
+
|
|
212
|
+
@Component({
|
|
213
|
+
selector: 'app-alert-demo',
|
|
214
|
+
standalone: true,
|
|
215
|
+
imports: [AlertComponent, ButtonComponent],
|
|
216
|
+
template: \`
|
|
217
|
+
<div class="flex flex-col gap-3">
|
|
218
|
+
@for (v of variants; track v) {
|
|
219
|
+
<africanies-alert
|
|
220
|
+
[variant]="v"
|
|
221
|
+
[title]="titles[v]"
|
|
222
|
+
[message]="messages[v]"
|
|
223
|
+
/>
|
|
224
|
+
}
|
|
225
|
+
</div>
|
|
226
|
+
\`,
|
|
227
|
+
})
|
|
228
|
+
export class AlertDemo {
|
|
229
|
+
readonly variants: AlertVariant[] = ['info', 'success', 'warning', 'danger'];
|
|
230
|
+
|
|
231
|
+
readonly titles: Record<AlertVariant, string> = {
|
|
232
|
+
info: 'Document checklist',
|
|
233
|
+
success: 'Submitted',
|
|
234
|
+
warning: 'Rates may be stale',
|
|
235
|
+
danger: 'Cannot release',
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
readonly messages: Record<AlertVariant, string> = {
|
|
239
|
+
info: 'Commercial invoice and packing list are still required.',
|
|
240
|
+
success: 'Shipment SFN-1042 was submitted to the carrier.',
|
|
241
|
+
warning: 'Last rate pull was more than 4 hours ago.',
|
|
242
|
+
danger: 'Customs hold blocks release until documents are approved.',
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
`,Hi=`
|
|
246
|
+
// Dismissible by default \u2014 listen to (dismissed) and unmount with @if.
|
|
247
|
+
// Set [dismissible]="false" when the notice has to stay.
|
|
248
|
+
|
|
249
|
+
import { Component, signal } from '@angular/core';
|
|
250
|
+
import { AlertComponent, ButtonComponent } from '@africanies/africanies-ui';
|
|
251
|
+
|
|
252
|
+
@Component({
|
|
253
|
+
selector: 'app-alert-dismiss-demo',
|
|
254
|
+
standalone: true,
|
|
255
|
+
imports: [AlertComponent, ButtonComponent],
|
|
256
|
+
template: \`
|
|
257
|
+
@if (showBanner()) {
|
|
258
|
+
<africanies-alert
|
|
259
|
+
variant="warning"
|
|
260
|
+
title="Rates outdated"
|
|
261
|
+
message="Refresh to pull the latest carrier rates before quoting."
|
|
262
|
+
(dismissed)="showBanner.set(false)"
|
|
263
|
+
>
|
|
264
|
+
<button
|
|
265
|
+
africanies-button
|
|
266
|
+
type="button"
|
|
267
|
+
size="sm"
|
|
268
|
+
class="mt-2"
|
|
269
|
+
(click)="refresh()"
|
|
270
|
+
>
|
|
271
|
+
Refresh rates
|
|
272
|
+
</button>
|
|
273
|
+
</africanies-alert>
|
|
274
|
+
} @else {
|
|
275
|
+
<button africanies-button type="button" variant="secondary" size="sm" (click)="showBanner.set(true)">
|
|
276
|
+
Show banner again
|
|
277
|
+
</button>
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
<!-- Permanent notice \u2014 no close control -->
|
|
281
|
+
<africanies-alert
|
|
282
|
+
class="mt-4 block"
|
|
283
|
+
variant="info"
|
|
284
|
+
message="This environment is connected to the staging API."
|
|
285
|
+
[dismissible]="false"
|
|
286
|
+
/>
|
|
287
|
+
\`,
|
|
288
|
+
})
|
|
289
|
+
export class AlertDismissDemo {
|
|
290
|
+
readonly showBanner = signal(true);
|
|
291
|
+
|
|
292
|
+
refresh(): void {
|
|
293
|
+
// refetch rates\u2026
|
|
294
|
+
this.showBanner.set(false);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
`;var qi=`// Domain services in @africanies/africanies-core own paths and mapping.
|
|
298
|
+
// Apps call inject(CountryService) / CurrencyService / PaymentMethodService / ProductService / \u2026 \u2014 or ApiClient for custom routes.
|
|
299
|
+
//
|
|
300
|
+
// List GET convention (ResourceId) \u2014 built-in services AND custom calls:
|
|
301
|
+
// null \u2192 paginated page GET {base} (+ page/size/order)
|
|
302
|
+
// 'all' \u2192 full dump GET {base}/all
|
|
303
|
+
// 42 \u2192 single record GET {base}/42
|
|
304
|
+
//
|
|
305
|
+
// Custom endpoints \u2014 IDE helpers on ApiClient:
|
|
306
|
+
// api.getResourcePage<T>('shipments', { page: 1 })
|
|
307
|
+
// api.getResourceAll<T>('shipments')
|
|
308
|
+
// api.getResourceById<T>('shipments', 42)
|
|
309
|
+
// // or overloads: getResource(base, null | 'all' | number, query?, options?)
|
|
310
|
+
// // Per-request mode override (tab mode unchanged):
|
|
311
|
+
// api.post('/claim', body, { shippingMode: 'stn' })
|
|
312
|
+
// // path helper: buildResourcePath('/my/read', id)
|
|
313
|
+
|
|
314
|
+
import { ApplicationConfig } from '@angular/core';
|
|
315
|
+
import {
|
|
316
|
+
provideAfricaniesSdk,
|
|
317
|
+
provideAfricaniesHttpClient,
|
|
318
|
+
} from '@africanies/africanies-core';
|
|
319
|
+
|
|
320
|
+
export const appConfig: ApplicationConfig = {
|
|
321
|
+
providers: [
|
|
322
|
+
provideAfricaniesSdk({
|
|
323
|
+
baseUrl: 'https://test-api-export.africaniestest.com/api',
|
|
324
|
+
// loadModeConfig: true by default \u2192 GET /public/mode/config on startup
|
|
325
|
+
}),
|
|
326
|
+
provideAfricaniesHttpClient(),
|
|
327
|
+
],
|
|
328
|
+
};
|
|
329
|
+
`,$i=`// GET /public/country/read/{id?} \u2014 ResourceId: null | 'all' | number.
|
|
330
|
+
// Flag images: countryFlagUrl(iso2) or mapCountrySelectOptions(rows) \u2192 prefixImageUrl
|
|
331
|
+
|
|
332
|
+
import { Component, inject, signal } from '@angular/core';
|
|
333
|
+
import { CountryService, mapCountrySelectOptions } from '@africanies/africanies-core';
|
|
334
|
+
import type { SelectOption } from '@africanies/africanies-ui';
|
|
335
|
+
import { firstValueFrom } from 'rxjs';
|
|
336
|
+
|
|
337
|
+
@Component({
|
|
338
|
+
selector: 'app-country-loader',
|
|
339
|
+
standalone: true,
|
|
340
|
+
template: \`
|
|
341
|
+
<africanies-select
|
|
342
|
+
label="Destination country"
|
|
343
|
+
[options]="countryOptions()"
|
|
344
|
+
searchable
|
|
345
|
+
/>
|
|
346
|
+
\`,
|
|
347
|
+
})
|
|
348
|
+
export class CountryLoaderComponent {
|
|
349
|
+
private readonly countriesApi = inject(CountryService);
|
|
350
|
+
protected readonly countryOptions = signal<SelectOption<number>[]>([]);
|
|
351
|
+
|
|
352
|
+
async ngOnInit(): Promise<void> {
|
|
353
|
+
const res = await firstValueFrom(this.countriesApi.readAll());
|
|
354
|
+
if (res.success && res.data) {
|
|
355
|
+
this.countryOptions.set(
|
|
356
|
+
mapCountrySelectOptions(res.data, { width: 40 }).map((row) => ({
|
|
357
|
+
label: row.label,
|
|
358
|
+
value: row.value,
|
|
359
|
+
prefixImageUrl: row.prefixImageUrl,
|
|
360
|
+
})),
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
`,Ki=`// GET /public/service/read/{id?} \u2014 ResourceId: null | 'all' | number.
|
|
366
|
+
// Public catalog \u2014 no auth required. App Settings Services / Plans boards.
|
|
367
|
+
// readPage({ page, order, search, size, from, to }) \u2192 ServiceModel[]
|
|
368
|
+
|
|
369
|
+
import { Component, inject, signal } from '@angular/core';
|
|
370
|
+
import { ServiceService } from '@africanies/africanies-core';
|
|
371
|
+
import type { ServiceModel } from '@africanies/africanies-models';
|
|
372
|
+
import { firstValueFrom } from 'rxjs';
|
|
373
|
+
|
|
374
|
+
@Component({
|
|
375
|
+
selector: 'app-service-loader',
|
|
376
|
+
standalone: true,
|
|
377
|
+
template: \`
|
|
378
|
+
<p>{{ services().length }} services</p>
|
|
379
|
+
@if (services()[0]; as first) {
|
|
380
|
+
<p>{{ first.name }}</p>
|
|
381
|
+
}
|
|
382
|
+
\`,
|
|
383
|
+
})
|
|
384
|
+
export class ServiceLoaderComponent {
|
|
385
|
+
private readonly servicesApi = inject(ServiceService);
|
|
386
|
+
protected readonly services = signal<ServiceModel[]>([]);
|
|
387
|
+
|
|
388
|
+
async ngOnInit(): Promise<void> {
|
|
389
|
+
const res = await firstValueFrom(
|
|
390
|
+
this.servicesApi.readPage({ page: 1, order: 'desc' }),
|
|
391
|
+
);
|
|
392
|
+
if (res.success && res.data) {
|
|
393
|
+
this.services.set(res.data);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
`,Yi=`// GET /public/document/read/{id?} \u2014 catalog preview by document id.
|
|
398
|
+
// readById(n) \u2192 bind doc.file_ref.base_64 in <img> / PDF viewer.
|
|
399
|
+
// For raw file_ref on shipments/KYC/waybills \u2192 FileService.read(ref).
|
|
400
|
+
|
|
401
|
+
import { Component, inject, signal } from '@angular/core';
|
|
402
|
+
import { DocumentService } from '@africanies/africanies-core';
|
|
403
|
+
import type { DocumentModel } from '@africanies/africanies-models';
|
|
404
|
+
import { firstValueFrom } from 'rxjs';
|
|
405
|
+
|
|
406
|
+
@Component({
|
|
407
|
+
selector: 'app-document-loader',
|
|
408
|
+
standalone: true,
|
|
409
|
+
template: \`
|
|
410
|
+
@if (doc(); as d) {
|
|
411
|
+
<p>{{ d.name }}</p>
|
|
412
|
+
@if (d.file_ref?.base_64) {
|
|
413
|
+
<img [src]="d.file_ref.base_64" alt="" />
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
\`,
|
|
417
|
+
})
|
|
418
|
+
export class DocumentLoaderComponent {
|
|
419
|
+
private readonly documentsApi = inject(DocumentService);
|
|
420
|
+
protected readonly doc = signal<DocumentModel | null>(null);
|
|
421
|
+
|
|
422
|
+
async load(id: number): Promise<void> {
|
|
423
|
+
const res = await firstValueFrom(this.documentsApi.readById(id));
|
|
424
|
+
if (res.success && res.data) {
|
|
425
|
+
this.doc.set(res.data);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
`,Zi=`// GET /public/plan/read/{id?} \u2014 ResourceId: null | 'all' | number.
|
|
430
|
+
// Public catalog \u2014 no auth required. App Settings Plans / Plan Packages.
|
|
431
|
+
// readPage({ page, order, search, size, from, to }) \u2192 PlanModel[] (nested packages)
|
|
432
|
+
|
|
433
|
+
import { Component, inject, signal } from '@angular/core';
|
|
434
|
+
import { PlanService } from '@africanies/africanies-core';
|
|
435
|
+
import type { PlanModel } from '@africanies/africanies-models';
|
|
436
|
+
import { firstValueFrom } from 'rxjs';
|
|
437
|
+
|
|
438
|
+
@Component({
|
|
439
|
+
selector: 'app-plan-loader',
|
|
440
|
+
standalone: true,
|
|
441
|
+
template: \`
|
|
442
|
+
<p>{{ plans().length }} plans</p>
|
|
443
|
+
@if (plans()[0]; as first) {
|
|
444
|
+
<p>{{ first.name }} \u2014 {{ first.packages.length }} packages</p>
|
|
445
|
+
}
|
|
446
|
+
\`,
|
|
447
|
+
})
|
|
448
|
+
export class PlanLoaderComponent {
|
|
449
|
+
private readonly plansApi = inject(PlanService);
|
|
450
|
+
protected readonly plans = signal<PlanModel[]>([]);
|
|
451
|
+
|
|
452
|
+
async ngOnInit(): Promise<void> {
|
|
453
|
+
const res = await firstValueFrom(this.plansApi.readPage({ page: 1 }));
|
|
454
|
+
if (res.success && res.data) {
|
|
455
|
+
this.plans.set(res.data);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
`,Qi=`// GET /currency/read/{id?} \u2014 ResourceId: null | 'all' | number.
|
|
460
|
+
// POST /currency/create \xB7 PUT /currency/update \xB7 DELETE /currency/delete ({ id } body).
|
|
461
|
+
// Laravel paginator in data is flattened to data[] + pagination.
|
|
462
|
+
// readPage({ page, order, search, from, to }) \u2192 CurrencyModel[]
|
|
463
|
+
// create / update serialize active + is_naira_greater to "1" / "0".
|
|
464
|
+
// After each write: show res.message, then readPage again.
|
|
465
|
+
|
|
466
|
+
import { Component, inject, signal } from '@angular/core';
|
|
467
|
+
import { CurrencyService, PaymentMethodService } from '@africanies/africanies-core';
|
|
468
|
+
import type { CurrencyModel, PaginationMetaModel } from '@africanies/africanies-models';
|
|
469
|
+
import { firstValueFrom } from 'rxjs';
|
|
470
|
+
|
|
471
|
+
@Component({
|
|
472
|
+
selector: 'app-currency-loader',
|
|
473
|
+
standalone: true,
|
|
474
|
+
template: \`
|
|
475
|
+
<p>{{ currencies().length }} currencies</p>
|
|
476
|
+
@if (currencies()[0]; as first) {
|
|
477
|
+
<p>{{ first.name }} ({{ first.short_code }})</p>
|
|
478
|
+
}
|
|
479
|
+
\`,
|
|
480
|
+
})
|
|
481
|
+
export class CurrencyLoaderComponent {
|
|
482
|
+
private readonly currenciesApi = inject(CurrencyService);
|
|
483
|
+
private readonly paymentMethodsApi = inject(PaymentMethodService);
|
|
484
|
+
protected readonly currencies = signal<CurrencyModel[]>([]);
|
|
485
|
+
protected readonly meta = signal<PaginationMetaModel | null>(null);
|
|
486
|
+
|
|
487
|
+
async ngOnInit(): Promise<void> {
|
|
488
|
+
const methods = await firstValueFrom(
|
|
489
|
+
this.paymentMethodsApi.readPage({ page: 1 }),
|
|
490
|
+
);
|
|
491
|
+
const page = await firstValueFrom(
|
|
492
|
+
this.currenciesApi.readPage({
|
|
493
|
+
page: 1,
|
|
494
|
+
order: 'desc',
|
|
495
|
+
size: 15,
|
|
496
|
+
search: '',
|
|
497
|
+
from: '',
|
|
498
|
+
to: '',
|
|
499
|
+
}),
|
|
500
|
+
);
|
|
501
|
+
if (page.success && page.data) {
|
|
502
|
+
this.currencies.set(page.data);
|
|
503
|
+
this.meta.set(page.pagination);
|
|
504
|
+
}
|
|
505
|
+
void methods;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async saveUsd(methodIds: number[]): Promise<void> {
|
|
509
|
+
const res = await firstValueFrom(
|
|
510
|
+
this.currenciesApi.create({
|
|
511
|
+
name: 'United States Dollar',
|
|
512
|
+
short_code: 'USD',
|
|
513
|
+
multiplication_rate: '1600',
|
|
514
|
+
division_rate: '1400',
|
|
515
|
+
active: true,
|
|
516
|
+
is_naira_greater: false,
|
|
517
|
+
payment_method_ids: methodIds,
|
|
518
|
+
}),
|
|
519
|
+
);
|
|
520
|
+
if (res.success) {
|
|
521
|
+
// toast res.message, close modal, then readPage again
|
|
522
|
+
await firstValueFrom(
|
|
523
|
+
this.currenciesApi.readPage({ page: 1, order: 'desc', size: 15 }),
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
`,Xi=`// GET /payment_method/read/{id?} \u2014 ResourceId: null | 'all' | number.
|
|
529
|
+
// PUT /payment_method/update \u2014 active toggle only (resend name + model from the row).
|
|
530
|
+
// No create/delete on this board. After update: toast message + patch updated_at locally.
|
|
531
|
+
// Laravel paginator in data is flattened to data[] + pagination.
|
|
532
|
+
// readPage({ page, size, order, search, from, to }) \u2192 PaymentMethodModel[]
|
|
533
|
+
// readAll() \u2192 full PaymentMethodModel[]
|
|
534
|
+
// readById(4) \u2192 single PaymentMethodModel
|
|
535
|
+
|
|
536
|
+
import { Component, inject, signal } from '@angular/core';
|
|
537
|
+
import { PaymentMethodService } from '@africanies/africanies-core';
|
|
538
|
+
import type { PaginationMetaModel, PaymentMethodModel } from '@africanies/africanies-models';
|
|
539
|
+
import { firstValueFrom } from 'rxjs';
|
|
540
|
+
|
|
541
|
+
@Component({
|
|
542
|
+
selector: 'app-payment-method-loader',
|
|
543
|
+
standalone: true,
|
|
544
|
+
template: \`
|
|
545
|
+
<p>{{ methods().length }} payment methods</p>
|
|
546
|
+
@if (methods()[0]; as first) {
|
|
547
|
+
<p>{{ first.name }} \u2014 {{ first.currencies.length }} currencies</p>
|
|
548
|
+
}
|
|
549
|
+
\`,
|
|
550
|
+
})
|
|
551
|
+
export class PaymentMethodLoaderComponent {
|
|
552
|
+
private readonly methodsApi = inject(PaymentMethodService);
|
|
553
|
+
protected readonly methods = signal<PaymentMethodModel[]>([]);
|
|
554
|
+
protected readonly meta = signal<PaginationMetaModel | null>(null);
|
|
555
|
+
|
|
556
|
+
async ngOnInit(): Promise<void> {
|
|
557
|
+
const page = await firstValueFrom(
|
|
558
|
+
this.methodsApi.readPage({ page: 1, order: 'desc' }),
|
|
559
|
+
);
|
|
560
|
+
if (page.success && page.data) {
|
|
561
|
+
this.methods.set(page.data);
|
|
562
|
+
this.meta.set(page.pagination);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async toggleActive(row: PaymentMethodModel, next: boolean): Promise<void> {
|
|
567
|
+
const res = await firstValueFrom(
|
|
568
|
+
this.methodsApi.update({
|
|
569
|
+
id: row.id,
|
|
570
|
+
name: row.name,
|
|
571
|
+
model: row.model,
|
|
572
|
+
active: next,
|
|
573
|
+
}),
|
|
574
|
+
);
|
|
575
|
+
if (res.success) {
|
|
576
|
+
// toast res.message; patch row.active + row.updated_at in place \u2014 do not readPage()
|
|
577
|
+
this.methods.update((list) =>
|
|
578
|
+
list.map((m) =>
|
|
579
|
+
m.id === row.id
|
|
580
|
+
? { ...m, active: next, updated_at: new Date().toISOString() }
|
|
581
|
+
: m,
|
|
582
|
+
),
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
`,Ji=`// GET /public/mode/config \u2014 usually loaded at bootstrap via provideAfricaniesSdk.
|
|
588
|
+
// ModeConfigService hydrates storage; getRegionConfig(country) reads STN/SFN units.
|
|
589
|
+
|
|
590
|
+
import { Component, inject } from '@angular/core';
|
|
591
|
+
import { ModeConfigService } from '@africanies/africanies-core';
|
|
592
|
+
|
|
593
|
+
@Component({
|
|
594
|
+
selector: 'app-region-units',
|
|
595
|
+
standalone: true,
|
|
596
|
+
template: \`
|
|
597
|
+
@if (region(); as r) {
|
|
598
|
+
<p>{{ r.currency_symbol }} \xB7 {{ r.mass_unit }} \xB7 {{ r.dimension_unit }}</p>
|
|
599
|
+
}
|
|
600
|
+
\`,
|
|
601
|
+
})
|
|
602
|
+
export class RegionUnitsComponent {
|
|
603
|
+
private readonly modeConfig = inject(ModeConfigService);
|
|
604
|
+
|
|
605
|
+
region() {
|
|
606
|
+
return this.modeConfig.getRegionConfig('ng');
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
reload() {
|
|
610
|
+
this.modeConfig.loadConfig().subscribe();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
`,er=`// GET /shipment_method/read/{id?} \u2014 ResourceId: null | 'all' | number.
|
|
614
|
+
// readPage() \u2192 paginated \xB7 readAll() \u2192 full \xB7 readById(n) \u2192 single ShipmentMethodModel
|
|
615
|
+
|
|
616
|
+
import { Component, inject, signal } from '@angular/core';
|
|
617
|
+
import { ShipmentMethodService } from '@africanies/africanies-core';
|
|
618
|
+
import type { ShipmentMethodModel } from '@africanies/africanies-models';
|
|
619
|
+
import { firstValueFrom } from 'rxjs';
|
|
620
|
+
|
|
621
|
+
@Component({
|
|
622
|
+
selector: 'app-carrier-loader',
|
|
623
|
+
standalone: true,
|
|
624
|
+
template: \`
|
|
625
|
+
<p>{{ methods().length }} methods</p>
|
|
626
|
+
@if (methods()[0]; as first) {
|
|
627
|
+
<p>{{ first.name }} \xB7 {{ first.mode }} \xB7 {{ first.zone_values.total }} zones</p>
|
|
628
|
+
}
|
|
629
|
+
\`,
|
|
630
|
+
})
|
|
631
|
+
export class CarrierLoaderComponent {
|
|
632
|
+
private readonly methodsApi = inject(ShipmentMethodService);
|
|
633
|
+
protected readonly methods = signal<ShipmentMethodModel[]>([]);
|
|
634
|
+
|
|
635
|
+
async ngOnInit(): Promise<void> {
|
|
636
|
+
const res = await firstValueFrom(this.methodsApi.readAll());
|
|
637
|
+
if (res.success && res.data) {
|
|
638
|
+
this.methods.set(res.data);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
`,tr=`// GET /warehouse/read/{id?} \u2014 ResourceId: null | 'all' | number.
|
|
643
|
+
// readPage() \u2192 paginated \xB7 readAll() \u2192 full \xB7 readById(n) \u2192 single WarehouseModel
|
|
644
|
+
|
|
645
|
+
import { Component, inject, signal } from '@angular/core';
|
|
646
|
+
import { WarehouseService } from '@africanies/africanies-core';
|
|
647
|
+
import type { WarehouseModel } from '@africanies/africanies-models';
|
|
648
|
+
import { firstValueFrom } from 'rxjs';
|
|
649
|
+
|
|
650
|
+
@Component({
|
|
651
|
+
selector: 'app-warehouse-loader',
|
|
652
|
+
standalone: true,
|
|
653
|
+
template: \`
|
|
654
|
+
<p>{{ warehouses().length }} warehouses</p>
|
|
655
|
+
@if (warehouses()[0]; as first) {
|
|
656
|
+
<p>{{ first.name }} \xB7 {{ first.city }} \xB7 {{ first.country?.iso2 }}</p>
|
|
657
|
+
}
|
|
658
|
+
\`,
|
|
659
|
+
})
|
|
660
|
+
export class WarehouseLoaderComponent {
|
|
661
|
+
private readonly warehousesApi = inject(WarehouseService);
|
|
662
|
+
protected readonly warehouses = signal<WarehouseModel[]>([]);
|
|
663
|
+
|
|
664
|
+
async ngOnInit(): Promise<void> {
|
|
665
|
+
const res = await firstValueFrom(this.warehousesApi.readAll());
|
|
666
|
+
if (res.success && res.data) {
|
|
667
|
+
this.warehouses.set(res.data);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
`,nr=`// GET /zone/read/records/{id?} \u2014 ResourceId: null | 'all' | number.
|
|
672
|
+
// readPage() \u2192 paginated \xB7 readAll() \u2192 full \xB7 readById(n) \u2192 single ZoneModel
|
|
673
|
+
|
|
674
|
+
import { Component, inject, signal } from '@angular/core';
|
|
675
|
+
import { ZoneService } from '@africanies/africanies-core';
|
|
676
|
+
import type { ZoneModel } from '@africanies/africanies-models';
|
|
677
|
+
import { firstValueFrom } from 'rxjs';
|
|
678
|
+
|
|
679
|
+
@Component({
|
|
680
|
+
selector: 'app-zone-loader',
|
|
681
|
+
standalone: true,
|
|
682
|
+
template: \`
|
|
683
|
+
<p>{{ zones().length }} zones</p>
|
|
684
|
+
@if (zones()[0]; as first) {
|
|
685
|
+
<p>{{ first.name }} \xB7 {{ first.type }}</p>
|
|
686
|
+
}
|
|
687
|
+
\`,
|
|
688
|
+
})
|
|
689
|
+
export class ZoneLoaderComponent {
|
|
690
|
+
private readonly zonesApi = inject(ZoneService);
|
|
691
|
+
protected readonly zones = signal<ZoneModel[]>([]);
|
|
692
|
+
|
|
693
|
+
async ngOnInit(): Promise<void> {
|
|
694
|
+
const res = await firstValueFrom(this.zonesApi.readAll());
|
|
695
|
+
if (res.success && res.data) {
|
|
696
|
+
this.zones.set(res.data);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
`,ir=`// POST /auth/forgot/password \u2014 email-only, unauthenticated. Body { email }.
|
|
701
|
+
// The backend emails a reset link. This frontend never shows a new-password
|
|
702
|
+
// form for that link (no token in the route).
|
|
703
|
+
// /onboarding/reset-password is a different flow (default_password first login).
|
|
704
|
+
// Admins can call the same forgot(email) from user/partner screens.
|
|
705
|
+
|
|
706
|
+
import { Component, computed, inject, signal } from '@angular/core';
|
|
707
|
+
import { RouterLink } from '@angular/router';
|
|
708
|
+
import { AuthService, isValidEmail } from '@africanies/africanies-core';
|
|
709
|
+
import { ButtonComponent, TextInputComponent } from '@africanies/africanies-ui';
|
|
710
|
+
import { firstValueFrom } from 'rxjs';
|
|
711
|
+
|
|
712
|
+
@Component({
|
|
713
|
+
selector: 'app-forgot-password',
|
|
714
|
+
standalone: true,
|
|
715
|
+
imports: [ButtonComponent, TextInputComponent, RouterLink],
|
|
716
|
+
template: \`
|
|
717
|
+
<p>We will send you a password reset link.</p>
|
|
718
|
+
<africanies-text-input
|
|
719
|
+
type="email"
|
|
720
|
+
label="Email"
|
|
721
|
+
autocomplete="email"
|
|
722
|
+
[(value)]="email"
|
|
723
|
+
/>
|
|
724
|
+
<button
|
|
725
|
+
africanies-button
|
|
726
|
+
type="button"
|
|
727
|
+
variant="primary"
|
|
728
|
+
[loading]="sending()"
|
|
729
|
+
[disabled]="!canSubmit()"
|
|
730
|
+
(click)="forgot()"
|
|
731
|
+
>
|
|
732
|
+
Send reset link
|
|
733
|
+
</button>
|
|
734
|
+
@if (status(); as copy) {
|
|
735
|
+
<p>{{ copy }}</p>
|
|
736
|
+
<a routerLink="/onboarding/login">Reset completed? Login here</a>
|
|
737
|
+
}
|
|
738
|
+
\`,
|
|
739
|
+
})
|
|
740
|
+
export class ForgotPasswordComponent {
|
|
741
|
+
private readonly authApi = inject(AuthService);
|
|
742
|
+
|
|
743
|
+
protected readonly email = signal('');
|
|
744
|
+
protected readonly sending = signal(false);
|
|
745
|
+
protected readonly status = signal<string | null>(null);
|
|
746
|
+
protected readonly canSubmit = computed(() => isValidEmail(this.email()));
|
|
747
|
+
|
|
748
|
+
protected async forgot(): Promise<void> {
|
|
749
|
+
if (!this.canSubmit()) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
this.sending.set(true);
|
|
753
|
+
this.status.set(null);
|
|
754
|
+
try {
|
|
755
|
+
const res = await firstValueFrom(this.authApi.forgot(this.email()));
|
|
756
|
+
if (res.success) {
|
|
757
|
+
this.status.set(res.message?.trim() || 'We have emailed your reset password.');
|
|
758
|
+
} else {
|
|
759
|
+
this.status.set(res.message?.trim() || 'Could not send a reset email.');
|
|
760
|
+
}
|
|
761
|
+
} finally {
|
|
762
|
+
this.sending.set(false);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
`,rr=`// GET /user \u2014 bare user object (no { success, data } wrapper).
|
|
767
|
+
// POST /user/change/password \u2014 first-login default password.
|
|
768
|
+
// POST /user/logout-from-all-sessions \u2014 while the token is still set, then clear().
|
|
769
|
+
// After login/register in your app:
|
|
770
|
+
// inject(AuthTokenService).set(access_token);
|
|
771
|
+
// UserService.me() then sends Authorization: Bearer \u2026
|
|
772
|
+
|
|
773
|
+
import { Component, inject, signal } from '@angular/core';
|
|
774
|
+
import { AuthTokenService, UserService } from '@africanies/africanies-core';
|
|
775
|
+
import type { UserModel } from '@africanies/africanies-models';
|
|
776
|
+
import { firstValueFrom } from 'rxjs';
|
|
777
|
+
|
|
778
|
+
@Component({
|
|
779
|
+
selector: 'app-profile',
|
|
780
|
+
standalone: true,
|
|
781
|
+
template: \`
|
|
782
|
+
@if (user(); as u) {
|
|
783
|
+
<p>{{ u.name }} \xB7 {{ u.email }} \xB7 {{ u.main_region }}</p>
|
|
784
|
+
}
|
|
785
|
+
\`,
|
|
786
|
+
})
|
|
787
|
+
export class ProfileComponent {
|
|
788
|
+
private readonly users = inject(UserService);
|
|
789
|
+
private readonly auth = inject(AuthTokenService);
|
|
790
|
+
protected readonly user = signal<UserModel | null>(null);
|
|
791
|
+
|
|
792
|
+
async ngOnInit(): Promise<void> {
|
|
793
|
+
// typically called right after login elsewhere:
|
|
794
|
+
// this.auth.set(loginResponse.access_token);
|
|
795
|
+
const res = await firstValueFrom(this.users.me());
|
|
796
|
+
if (res.success && res.data) {
|
|
797
|
+
this.user.set(res.data);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
logout(): void {
|
|
802
|
+
this.users.logoutFromAllSessions().subscribe({
|
|
803
|
+
next: () => this.auth.clear(),
|
|
804
|
+
error: () => this.auth.clear(),
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
`,or=`// GET /user/notifications/read/{id?} \u2014 auth required (ResourceId + UUID by id).
|
|
809
|
+
// PUT /user/notifications/update \u2014 markRead(id) or markRead() for all
|
|
810
|
+
//
|
|
811
|
+
// Header badge: first page via readPage(). Drawer: infinite scroll via onNotificationLoadPage.
|
|
812
|
+
|
|
813
|
+
import { Component, inject, signal } from '@angular/core';
|
|
814
|
+
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
|
815
|
+
import {
|
|
816
|
+
AuthTokenService,
|
|
817
|
+
mapNotificationInboxItem,
|
|
818
|
+
NotificationService,
|
|
819
|
+
} from '@africanies/africanies-core';
|
|
820
|
+
import type { NotificationModel } from '@africanies/africanies-models';
|
|
821
|
+
import { NOTIFICATION_PAGE_SIZE } from '@africanies/africanies-models';
|
|
822
|
+
import type { AfricaniesNotification, NotificationPageResult } from '@africanies/africanies-ui';
|
|
823
|
+
import { catchError, map, of, switchMap, tap } from 'rxjs';
|
|
824
|
+
|
|
825
|
+
@Component({
|
|
826
|
+
selector: 'app-header-notifications',
|
|
827
|
+
standalone: true,
|
|
828
|
+
template: \`
|
|
829
|
+
<africanies-app-shell-header
|
|
830
|
+
title="Dashboard"
|
|
831
|
+
[notifications]="notifications()"
|
|
832
|
+
[onNotificationLoadPage]="loadNotificationPage"
|
|
833
|
+
[onNotificationMarkRead]="markNotificationRead"
|
|
834
|
+
[onNotificationMarkAllRead]="markAllNotificationsRead"
|
|
835
|
+
/>
|
|
836
|
+
\`,
|
|
837
|
+
})
|
|
838
|
+
export class HeaderNotificationsComponent {
|
|
839
|
+
private readonly auth = inject(AuthTokenService);
|
|
840
|
+
private readonly notificationsApi = inject(NotificationService);
|
|
841
|
+
|
|
842
|
+
protected readonly notifications = signal<AfricaniesNotification[]>([]);
|
|
843
|
+
|
|
844
|
+
protected loadNotificationPage = (page: number) =>
|
|
845
|
+
this.notificationsApi
|
|
846
|
+
.readPage({ page, size: NOTIFICATION_PAGE_SIZE, order: 'desc' })
|
|
847
|
+
.pipe(
|
|
848
|
+
map(
|
|
849
|
+
(res): NotificationPageResult => ({
|
|
850
|
+
items:
|
|
851
|
+
res.success && res.data
|
|
852
|
+
? res.data.map((row) => this.toHeaderItem(row))
|
|
853
|
+
: [],
|
|
854
|
+
pagination: res.pagination ?? null,
|
|
855
|
+
}),
|
|
856
|
+
),
|
|
857
|
+
);
|
|
858
|
+
|
|
859
|
+
protected markNotificationRead = (id: string) =>
|
|
860
|
+
this.notificationsApi.markRead(id).pipe(
|
|
861
|
+
tap(() => this.patchRead(id)),
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
protected markAllNotificationsRead = () =>
|
|
865
|
+
this.notificationsApi.markRead().pipe(
|
|
866
|
+
tap(() =>
|
|
867
|
+
this.notifications.update((items) =>
|
|
868
|
+
items.map((item) => ({ ...item, read: true })),
|
|
869
|
+
),
|
|
870
|
+
),
|
|
871
|
+
);
|
|
872
|
+
|
|
873
|
+
constructor() {
|
|
874
|
+
toObservable(this.auth.token)
|
|
875
|
+
.pipe(
|
|
876
|
+
switchMap((token) => {
|
|
877
|
+
if (!token) {
|
|
878
|
+
return of([] as AfricaniesNotification[]);
|
|
879
|
+
}
|
|
880
|
+
return this.notificationsApi
|
|
881
|
+
.readPage({
|
|
882
|
+
page: 1,
|
|
883
|
+
size: NOTIFICATION_PAGE_SIZE,
|
|
884
|
+
order: 'desc',
|
|
885
|
+
})
|
|
886
|
+
.pipe(
|
|
887
|
+
map((res) =>
|
|
888
|
+
res.success && res.data
|
|
889
|
+
? res.data.map((row) => this.toHeaderItem(row))
|
|
890
|
+
: [],
|
|
891
|
+
),
|
|
892
|
+
catchError(() => of([] as AfricaniesNotification[])),
|
|
893
|
+
);
|
|
894
|
+
}),
|
|
895
|
+
takeUntilDestroyed(),
|
|
896
|
+
)
|
|
897
|
+
.subscribe((items) => this.notifications.set(items));
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
private patchRead(id: string): void {
|
|
901
|
+
this.notifications.update((items) =>
|
|
902
|
+
items.map((item) => (item.id === id ? { ...item, read: true } : item)),
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
private toHeaderItem(row: NotificationModel): AfricaniesNotification {
|
|
907
|
+
const item = mapNotificationInboxItem(row);
|
|
908
|
+
return {
|
|
909
|
+
id: item.id,
|
|
910
|
+
title: item.title,
|
|
911
|
+
body: item.body,
|
|
912
|
+
timestamp: item.timestamp,
|
|
913
|
+
read: item.read,
|
|
914
|
+
link: item.link,
|
|
915
|
+
externalLink: item.external_link,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
`,ar=`// GET /product/read/{id?} \u2014 ResourceId: null | 'all' | number.
|
|
920
|
+
// readPage() \u2192 paginated \xB7 readAll() \u2192 full \xB7 readById(n) \u2192 single ProductModel
|
|
921
|
+
|
|
922
|
+
import { Component, inject, OnInit, signal } from '@angular/core';
|
|
923
|
+
import { ProductService } from '@africanies/africanies-core';
|
|
924
|
+
import type { ProductModel } from '@africanies/africanies-models';
|
|
925
|
+
import { firstValueFrom } from 'rxjs';
|
|
926
|
+
|
|
927
|
+
@Component({
|
|
928
|
+
selector: 'app-product-loader',
|
|
929
|
+
standalone: true,
|
|
930
|
+
template: \`
|
|
931
|
+
<p>{{ products().length }} products</p>
|
|
932
|
+
@if (products()[0]; as first) {
|
|
933
|
+
<p>{{ first.name }} \u2014 {{ first.hs_code }}</p>
|
|
934
|
+
}
|
|
935
|
+
\`,
|
|
936
|
+
})
|
|
937
|
+
export class ProductLoaderComponent implements OnInit {
|
|
938
|
+
private readonly productsApi = inject(ProductService);
|
|
939
|
+
protected readonly products = signal<ProductModel[]>([]);
|
|
940
|
+
|
|
941
|
+
async ngOnInit(): Promise<void> {
|
|
942
|
+
const res = await firstValueFrom(this.productsApi.readAll());
|
|
943
|
+
if (res.success && res.data) {
|
|
944
|
+
this.products.set(res.data);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
`,sr=`// POST /file/read \u2014 body { ref } when a record stores file_ref.
|
|
949
|
+
// data.mime_type + data.base_64 for preview. Waybills: readMultiple(ref).
|
|
950
|
+
|
|
951
|
+
import { Component, inject, signal } from '@angular/core';
|
|
952
|
+
import { FileService } from '@africanies/africanies-core';
|
|
953
|
+
import type { FileReadModel } from '@africanies/africanies-models';
|
|
954
|
+
import { firstValueFrom } from 'rxjs';
|
|
955
|
+
|
|
956
|
+
@Component({
|
|
957
|
+
selector: 'app-file-loader',
|
|
958
|
+
standalone: true,
|
|
959
|
+
template: \`
|
|
960
|
+
@if (file(); as f) {
|
|
961
|
+
<p>{{ f.mime_type }}</p>
|
|
962
|
+
@if (f.base_64) {
|
|
963
|
+
<img [src]="f.base_64" alt="" />
|
|
964
|
+
} @else if (f.url) {
|
|
965
|
+
<a [href]="f.url" target="_blank" rel="noopener">Open signed URL</a>
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
\`,
|
|
969
|
+
})
|
|
970
|
+
export class FileLoaderComponent {
|
|
971
|
+
private readonly filesApi = inject(FileService);
|
|
972
|
+
protected readonly file = signal<FileReadModel | null>(null);
|
|
973
|
+
|
|
974
|
+
async load(ref: string): Promise<void> {
|
|
975
|
+
const res = await firstValueFrom(this.filesApi.read(ref));
|
|
976
|
+
if (res.success && res.data) {
|
|
977
|
+
this.file.set(res.data);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
`;var cr=`// Five variants. Primary and ghost-primary pick up SFN green / STN orange
|
|
982
|
+
// from ModeColorService \u2014 don't hard-code those colors yourself.
|
|
983
|
+
// ghost stays neutral (Cancel); ghost-primary is a mode-colored text button.
|
|
984
|
+
|
|
985
|
+
import { Component } from '@angular/core';
|
|
986
|
+
import {
|
|
987
|
+
ButtonComponent,
|
|
988
|
+
type ButtonVariant,
|
|
989
|
+
} from '@africanies/africanies-ui';
|
|
990
|
+
|
|
991
|
+
@Component({
|
|
992
|
+
selector: 'app-shipment-actions',
|
|
993
|
+
standalone: true,
|
|
994
|
+
imports: [ButtonComponent],
|
|
995
|
+
template: \`
|
|
996
|
+
<div class="flex flex-wrap items-end gap-6">
|
|
997
|
+
@for (v of variants; track v) {
|
|
998
|
+
<div class="flex flex-col gap-2">
|
|
999
|
+
<span class="text-caption text-neutral-600">{{ v }}</span>
|
|
1000
|
+
<button africanies-button type="button" [variant]="v">
|
|
1001
|
+
{{ labels[v] }}
|
|
1002
|
+
</button>
|
|
1003
|
+
</div>
|
|
1004
|
+
}
|
|
1005
|
+
</div>
|
|
1006
|
+
\`,
|
|
1007
|
+
})
|
|
1008
|
+
export class ShipmentActionsComponent {
|
|
1009
|
+
protected readonly variants: ButtonVariant[] = [
|
|
1010
|
+
'primary',
|
|
1011
|
+
'secondary',
|
|
1012
|
+
'ghost',
|
|
1013
|
+
'ghost-primary',
|
|
1014
|
+
'ghost-danger',
|
|
1015
|
+
'underline',
|
|
1016
|
+
'danger',
|
|
1017
|
+
];
|
|
1018
|
+
|
|
1019
|
+
protected readonly labels: Record<ButtonVariant, string> = {
|
|
1020
|
+
primary: 'Save shipment',
|
|
1021
|
+
secondary: 'Save draft',
|
|
1022
|
+
ghost: 'Cancel',
|
|
1023
|
+
'ghost-primary': 'Add row',
|
|
1024
|
+
'ghost-danger': 'Remove',
|
|
1025
|
+
underline: 'Learn more',
|
|
1026
|
+
danger: 'Delete',
|
|
1027
|
+
};
|
|
1028
|
+
}`,pr=`// sm | md | lg. Default is md. Use sm in toolbars/dialogs; save lg for hero CTAs.
|
|
1029
|
+
|
|
1030
|
+
import { Component } from '@angular/core';
|
|
1031
|
+
import {
|
|
1032
|
+
ButtonComponent,
|
|
1033
|
+
type ButtonSize,
|
|
1034
|
+
} from '@africanies/africanies-ui';
|
|
1035
|
+
|
|
1036
|
+
@Component({
|
|
1037
|
+
selector: 'app-button-sizes',
|
|
1038
|
+
standalone: true,
|
|
1039
|
+
imports: [ButtonComponent],
|
|
1040
|
+
template: \`
|
|
1041
|
+
<div class="flex flex-wrap items-end gap-6">
|
|
1042
|
+
@for (s of sizes; track s) {
|
|
1043
|
+
<div class="flex flex-col gap-2">
|
|
1044
|
+
<span class="text-caption text-neutral-600">size={{ s }}</span>
|
|
1045
|
+
<button africanies-button type="button" [size]="s">
|
|
1046
|
+
Continue
|
|
1047
|
+
</button>
|
|
1048
|
+
</div>
|
|
1049
|
+
}
|
|
1050
|
+
</div>
|
|
1051
|
+
\`,
|
|
1052
|
+
})
|
|
1053
|
+
export class ButtonSizesComponent {
|
|
1054
|
+
protected readonly sizes: ButtonSize[] = ['sm', 'md', 'lg'];
|
|
1055
|
+
}`,dr=`// Full variant \xD7 size grid \u2014 handy for QA, not something you'd ship on a product page.
|
|
1056
|
+
|
|
1057
|
+
import { Component } from '@angular/core';
|
|
1058
|
+
import {
|
|
1059
|
+
ButtonComponent,
|
|
1060
|
+
type ButtonSize,
|
|
1061
|
+
type ButtonVariant,
|
|
1062
|
+
} from '@africanies/africanies-ui';
|
|
1063
|
+
|
|
1064
|
+
@Component({
|
|
1065
|
+
selector: 'app-button-matrix',
|
|
1066
|
+
standalone: true,
|
|
1067
|
+
imports: [ButtonComponent],
|
|
1068
|
+
template: \`
|
|
1069
|
+
<table class="w-full border-collapse text-left">
|
|
1070
|
+
<thead>
|
|
1071
|
+
<tr class="text-caption text-neutral-600">
|
|
1072
|
+
<th class="pb-3 pr-4 font-medium">Variant</th>
|
|
1073
|
+
@for (s of sizes; track s) {
|
|
1074
|
+
<th class="pb-3 pr-4 font-medium">{{ s }}</th>
|
|
1075
|
+
}
|
|
1076
|
+
</tr>
|
|
1077
|
+
</thead>
|
|
1078
|
+
<tbody>
|
|
1079
|
+
@for (v of variants; track v) {
|
|
1080
|
+
<tr class="border-t border-border dark:border-white/10">
|
|
1081
|
+
<td class="py-3 pr-4 text-body-sm text-neutral-600">{{ v }}</td>
|
|
1082
|
+
@for (s of sizes; track s) {
|
|
1083
|
+
<td class="py-3 pr-4">
|
|
1084
|
+
<button africanies-button type="button" [variant]="v" [size]="s">
|
|
1085
|
+
Action
|
|
1086
|
+
</button>
|
|
1087
|
+
</td>
|
|
1088
|
+
}
|
|
1089
|
+
</tr>
|
|
1090
|
+
}
|
|
1091
|
+
</tbody>
|
|
1092
|
+
</table>
|
|
1093
|
+
\`,
|
|
1094
|
+
})
|
|
1095
|
+
export class ButtonMatrixComponent {
|
|
1096
|
+
protected readonly variants: ButtonVariant[] = [
|
|
1097
|
+
'primary',
|
|
1098
|
+
'secondary',
|
|
1099
|
+
'ghost',
|
|
1100
|
+
'danger',
|
|
1101
|
+
];
|
|
1102
|
+
protected readonly sizes: ButtonSize[] = ['sm', 'md', 'lg'];
|
|
1103
|
+
}`,mr=`// Drop <africanies-icon> inside the button \u2014 same focus ring as the label.
|
|
1104
|
+
// Icon-only? Put aria-label on the host.
|
|
1105
|
+
|
|
1106
|
+
import { Component } from '@angular/core';
|
|
1107
|
+
import { AfricaniesIconComponent } from '@africanies/africanies-icons';
|
|
1108
|
+
import { ButtonComponent } from '@africanies/africanies-ui';
|
|
1109
|
+
|
|
1110
|
+
@Component({
|
|
1111
|
+
selector: 'app-button-icons',
|
|
1112
|
+
standalone: true,
|
|
1113
|
+
imports: [ButtonComponent, AfricaniesIconComponent],
|
|
1114
|
+
template: \`
|
|
1115
|
+
<div class="flex flex-wrap gap-3">
|
|
1116
|
+
<button africanies-button type="button" variant="primary">
|
|
1117
|
+
<africanies-icon name="airplane" [size]="16" />
|
|
1118
|
+
Create shipment
|
|
1119
|
+
</button>
|
|
1120
|
+
|
|
1121
|
+
<button africanies-button type="button" variant="secondary">
|
|
1122
|
+
<africanies-icon name="adjust" [size]="16" />
|
|
1123
|
+
Filters
|
|
1124
|
+
</button>
|
|
1125
|
+
|
|
1126
|
+
<button africanies-button type="button" variant="ghost" size="sm">
|
|
1127
|
+
<africanies-icon name="warehouse" [size]="14" />
|
|
1128
|
+
Warehouse
|
|
1129
|
+
</button>
|
|
1130
|
+
|
|
1131
|
+
<button africanies-button type="button" variant="danger" size="sm">
|
|
1132
|
+
<africanies-icon name="warning" [size]="14" />
|
|
1133
|
+
Void
|
|
1134
|
+
</button>
|
|
1135
|
+
</div>
|
|
1136
|
+
\`,
|
|
1137
|
+
})
|
|
1138
|
+
export class ButtonIconsComponent {}`,ur=`// Same look on <a> \u2014 use routerLink for in-app, href for external.
|
|
1139
|
+
|
|
1140
|
+
import { Component } from '@angular/core';
|
|
1141
|
+
import { RouterLink } from '@angular/router';
|
|
1142
|
+
import { ButtonComponent } from '@africanies/africanies-ui';
|
|
1143
|
+
|
|
1144
|
+
@Component({
|
|
1145
|
+
selector: 'app-button-links',
|
|
1146
|
+
standalone: true,
|
|
1147
|
+
imports: [ButtonComponent, RouterLink],
|
|
1148
|
+
template: \`
|
|
1149
|
+
<div class="flex flex-wrap gap-3">
|
|
1150
|
+
<a africanies-button routerLink="/shipments/new" variant="primary">
|
|
1151
|
+
Primary link
|
|
1152
|
+
</a>
|
|
1153
|
+
|
|
1154
|
+
<a africanies-button routerLink="/drafts" variant="secondary" size="sm">
|
|
1155
|
+
Secondary link
|
|
1156
|
+
</a>
|
|
1157
|
+
|
|
1158
|
+
<a africanies-button href="/help/getting-started" variant="ghost">
|
|
1159
|
+
Ghost link
|
|
1160
|
+
</a>
|
|
1161
|
+
</div>
|
|
1162
|
+
\`,
|
|
1163
|
+
})
|
|
1164
|
+
export class ButtonLinksComponent {}`,fr=`// [disabled] works on buttons and anchors (anchors get aria-disabled + tabindex=-1).
|
|
1165
|
+
// Use this for unavailable actions \u2014 in-flight work belongs on [loading].
|
|
1166
|
+
|
|
1167
|
+
import { Component, signal } from '@angular/core';
|
|
1168
|
+
import { ButtonComponent } from '@africanies/africanies-ui';
|
|
1169
|
+
|
|
1170
|
+
@Component({
|
|
1171
|
+
selector: 'app-button-disabled',
|
|
1172
|
+
standalone: true,
|
|
1173
|
+
imports: [ButtonComponent],
|
|
1174
|
+
template: \`
|
|
1175
|
+
<div class="flex flex-wrap gap-3">
|
|
1176
|
+
<button africanies-button type="button" variant="primary" [disabled]="saving()">
|
|
1177
|
+
{{ saving() ? 'Saving\u2026' : 'Save shipment' }}
|
|
1178
|
+
</button>
|
|
1179
|
+
|
|
1180
|
+
<button africanies-button type="button" variant="secondary" disabled>
|
|
1181
|
+
Secondary
|
|
1182
|
+
</button>
|
|
1183
|
+
|
|
1184
|
+
<button africanies-button type="button" variant="ghost" disabled>
|
|
1185
|
+
Ghost
|
|
1186
|
+
</button>
|
|
1187
|
+
|
|
1188
|
+
<button africanies-button type="button" variant="danger" disabled>
|
|
1189
|
+
Danger
|
|
1190
|
+
</button>
|
|
1191
|
+
|
|
1192
|
+
<a africanies-button href="/shipments" variant="ghost" [disabled]="saving()">
|
|
1193
|
+
Ghost link
|
|
1194
|
+
</a>
|
|
1195
|
+
</div>
|
|
1196
|
+
\`,
|
|
1197
|
+
})
|
|
1198
|
+
export class ButtonDisabledComponent {
|
|
1199
|
+
protected readonly saving = signal(false);
|
|
1200
|
+
}`,gr=`// Default loading state is the circular spinner. Label stays in the layout
|
|
1201
|
+
// (invisible) so the button does not shrink while the request is in flight.
|
|
1202
|
+
|
|
1203
|
+
import { Component, signal } from '@angular/core';
|
|
1204
|
+
import { ButtonComponent } from '@africanies/africanies-ui';
|
|
1205
|
+
|
|
1206
|
+
@Component({
|
|
1207
|
+
selector: 'app-button-loading',
|
|
1208
|
+
standalone: true,
|
|
1209
|
+
imports: [ButtonComponent],
|
|
1210
|
+
template: \`
|
|
1211
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
1212
|
+
<button
|
|
1213
|
+
africanies-button
|
|
1214
|
+
type="button"
|
|
1215
|
+
variant="primary"
|
|
1216
|
+
[loading]="saving()"
|
|
1217
|
+
(click)="save()"
|
|
1218
|
+
>
|
|
1219
|
+
Save shipment
|
|
1220
|
+
</button>
|
|
1221
|
+
|
|
1222
|
+
<button africanies-button type="button" variant="secondary" [loading]="true">
|
|
1223
|
+
Saving draft
|
|
1224
|
+
</button>
|
|
1225
|
+
|
|
1226
|
+
<button
|
|
1227
|
+
africanies-button
|
|
1228
|
+
type="button"
|
|
1229
|
+
variant="danger"
|
|
1230
|
+
size="sm"
|
|
1231
|
+
[loading]="true"
|
|
1232
|
+
>
|
|
1233
|
+
Deleting
|
|
1234
|
+
</button>
|
|
1235
|
+
</div>
|
|
1236
|
+
\`,
|
|
1237
|
+
})
|
|
1238
|
+
export class ButtonLoadingComponent {
|
|
1239
|
+
protected readonly saving = signal(false);
|
|
1240
|
+
|
|
1241
|
+
protected save(): void {
|
|
1242
|
+
this.saving.set(true);
|
|
1243
|
+
// POST \u2192 finalize in subscribe/finalize and set saving false.
|
|
1244
|
+
}
|
|
1245
|
+
}`,hr=`// Typical toolbar: ghost cancel \u2192 secondary draft \u2192 primary submit.
|
|
1246
|
+
// Toggle SFN/STN in the sidebar to see primary shift color.
|
|
1247
|
+
|
|
1248
|
+
import { Component, inject, signal } from '@angular/core';
|
|
1249
|
+
import { ModeColorService } from '@africanies/africanies-theme';
|
|
1250
|
+
import { ButtonComponent } from '@africanies/africanies-ui';
|
|
1251
|
+
|
|
1252
|
+
@Component({
|
|
1253
|
+
selector: 'app-shipment-toolbar',
|
|
1254
|
+
standalone: true,
|
|
1255
|
+
imports: [ButtonComponent],
|
|
1256
|
+
template: \`
|
|
1257
|
+
<div
|
|
1258
|
+
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-border bg-background-welcome px-4 py-3 dark:border-white/10 dark:bg-ink-950"
|
|
1259
|
+
>
|
|
1260
|
+
<div>
|
|
1261
|
+
<p class="m-0 text-body font-medium text-ink dark:text-white">
|
|
1262
|
+
Shipment {{ ref() }}
|
|
1263
|
+
</p>
|
|
1264
|
+
<p class="m-0 text-caption text-neutral-600">
|
|
1265
|
+
Draft \xB7 {{ route() }}
|
|
1266
|
+
</p>
|
|
1267
|
+
</div>
|
|
1268
|
+
|
|
1269
|
+
<div class="flex flex-wrap gap-2">
|
|
1270
|
+
<button africanies-button type="button" variant="ghost" size="sm" (click)="cancel()">
|
|
1271
|
+
Cancel
|
|
1272
|
+
</button>
|
|
1273
|
+
|
|
1274
|
+
<button
|
|
1275
|
+
africanies-button
|
|
1276
|
+
type="button"
|
|
1277
|
+
variant="secondary"
|
|
1278
|
+
size="sm"
|
|
1279
|
+
[loading]="saving()"
|
|
1280
|
+
(click)="saveDraft()"
|
|
1281
|
+
>
|
|
1282
|
+
Save draft
|
|
1283
|
+
</button>
|
|
1284
|
+
|
|
1285
|
+
<button
|
|
1286
|
+
africanies-button
|
|
1287
|
+
type="button"
|
|
1288
|
+
variant="primary"
|
|
1289
|
+
size="sm"
|
|
1290
|
+
[loading]="saving()"
|
|
1291
|
+
(click)="submit()"
|
|
1292
|
+
>
|
|
1293
|
+
Submit
|
|
1294
|
+
</button>
|
|
1295
|
+
</div>
|
|
1296
|
+
</div>
|
|
1297
|
+
\`,
|
|
1298
|
+
})
|
|
1299
|
+
export class ShipmentToolbarComponent {
|
|
1300
|
+
protected readonly modeColor = inject(ModeColorService);
|
|
1301
|
+
|
|
1302
|
+
protected readonly ref = signal('SFN-1042');
|
|
1303
|
+
protected readonly route = signal('Lagos \u2192 London');
|
|
1304
|
+
protected readonly saving = signal(false);
|
|
1305
|
+
|
|
1306
|
+
protected cancel(): void {
|
|
1307
|
+
// Navigate away or close parent overlay ref.
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
protected saveDraft(): void {
|
|
1311
|
+
this.saving.set(true);
|
|
1312
|
+
// POST draft \u2192 finalize in subscribe/finalize and set saving false.
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
protected submit(): void {
|
|
1316
|
+
this.saving.set(true);
|
|
1317
|
+
// POST submit \u2192 on success close overlay or navigate to detail.
|
|
1318
|
+
}
|
|
1319
|
+
}`,br=`// Clipboard control \u2014 icon flips to check on success.
|
|
1320
|
+
// Optional [announce]="true" also fires a toast when ToastService is provided.
|
|
1321
|
+
|
|
1322
|
+
import { Component, signal } from '@angular/core';
|
|
1323
|
+
import { CopyButtonComponent } from '@africanies/africanies-ui';
|
|
1324
|
+
|
|
1325
|
+
@Component({
|
|
1326
|
+
selector: 'app-copy-demo',
|
|
1327
|
+
standalone: true,
|
|
1328
|
+
imports: [CopyButtonComponent],
|
|
1329
|
+
template: \`
|
|
1330
|
+
<div class="flex flex-wrap items-center gap-4">
|
|
1331
|
+
<code class="rounded-md bg-border/60 px-2 py-1 font-mono text-caption">
|
|
1332
|
+
{{ reference() }}
|
|
1333
|
+
</code>
|
|
1334
|
+
<africanies-copy [value]="reference()" ariaLabel="Copy reference" />
|
|
1335
|
+
<africanies-copy
|
|
1336
|
+
[value]="snippet()"
|
|
1337
|
+
label="Copy snippet"
|
|
1338
|
+
[announce]="true"
|
|
1339
|
+
/>
|
|
1340
|
+
</div>
|
|
1341
|
+
\`,
|
|
1342
|
+
})
|
|
1343
|
+
export class CopyDemoComponent {
|
|
1344
|
+
protected readonly reference = signal('SFN-1042');
|
|
1345
|
+
protected readonly snippet = signal('<africanies-icon name="airplane" />');
|
|
1346
|
+
}
|
|
1347
|
+
`;var vr=`
|
|
1348
|
+
<africanies-chip variant="neutral">Neutral</africanies-chip>
|
|
1349
|
+
<africanies-chip variant="success">Delivered</africanies-chip>
|
|
1350
|
+
<africanies-chip variant="warning">Pending</africanies-chip>
|
|
1351
|
+
<africanies-chip variant="danger">Exception</africanies-chip>
|
|
1352
|
+
<africanies-chip variant="export">SFN</africanies-chip>
|
|
1353
|
+
<africanies-chip variant="import">STN</africanies-chip>
|
|
1354
|
+
`.trim(),Sr=`
|
|
1355
|
+
<africanies-chip variant="warning" icon="clock">Pending</africanies-chip>
|
|
1356
|
+
|
|
1357
|
+
<africanies-chip
|
|
1358
|
+
variant="danger"
|
|
1359
|
+
[removable]="true"
|
|
1360
|
+
(removed)="clearFilter()"
|
|
1361
|
+
>
|
|
1362
|
+
Exception
|
|
1363
|
+
</africanies-chip>
|
|
1364
|
+
`.trim(),Cr=`
|
|
1365
|
+
<ng-template africaniesCellDef="status" let-row>
|
|
1366
|
+
<africanies-chip [variant]="statusVariant(row.status)">{{ row.status }}</africanies-chip>
|
|
1367
|
+
</ng-template>
|
|
1368
|
+
`.trim();var wr=`
|
|
1369
|
+
// Block for first load; inline for a quiet refresh in a toolbar or header.
|
|
1370
|
+
// Spinner color follows ModeColorService. Don\u2019t drop this inside africanies-table \u2014 use AsyncState.
|
|
1371
|
+
|
|
1372
|
+
import { Component } from '@angular/core';
|
|
1373
|
+
import { LoadingStateComponent } from '@africanies/africanies-ui';
|
|
1374
|
+
|
|
1375
|
+
@Component({
|
|
1376
|
+
selector: 'app-loading-demo',
|
|
1377
|
+
standalone: true,
|
|
1378
|
+
imports: [LoadingStateComponent],
|
|
1379
|
+
template: \`
|
|
1380
|
+
<div class="grid gap-6 md:grid-cols-2">
|
|
1381
|
+
<div class="flex flex-col gap-2">
|
|
1382
|
+
<p class="m-0 text-caption font-medium text-neutral-600">Block</p>
|
|
1383
|
+
<div
|
|
1384
|
+
class="flex min-h-[10rem] items-center justify-center rounded-lg border border-dashed border-border bg-background-welcome dark:border-white/10 dark:bg-ink-950"
|
|
1385
|
+
>
|
|
1386
|
+
<africanies-loading-state message="Loading shipments\u2026" />
|
|
1387
|
+
</div>
|
|
1388
|
+
</div>
|
|
1389
|
+
|
|
1390
|
+
<div class="flex flex-col gap-2">
|
|
1391
|
+
<p class="m-0 text-caption font-medium text-neutral-600">Inline</p>
|
|
1392
|
+
<div
|
|
1393
|
+
class="flex min-h-[10rem] items-center rounded-lg border border-dashed border-border bg-background-welcome px-5 dark:border-white/10 dark:bg-ink-950"
|
|
1394
|
+
>
|
|
1395
|
+
<africanies-loading-state mode="inline" message="Refreshing rates\u2026" />
|
|
1396
|
+
</div>
|
|
1397
|
+
</div>
|
|
1398
|
+
</div>
|
|
1399
|
+
\`,
|
|
1400
|
+
})
|
|
1401
|
+
export class LoadingDemoComponent {}
|
|
1402
|
+
`,Er=`
|
|
1403
|
+
// Fetch failed \u2014 always wire (retry) to your refetch. Message from the API layer is fine.
|
|
1404
|
+
// Section/page failures only; not field validation or a single missing row.
|
|
1405
|
+
|
|
1406
|
+
import { Component, inject, signal } from '@angular/core';
|
|
1407
|
+
import { ErrorStateComponent } from '@africanies/africanies-ui';
|
|
1408
|
+
|
|
1409
|
+
@Component({
|
|
1410
|
+
selector: 'app-error-demo',
|
|
1411
|
+
standalone: true,
|
|
1412
|
+
imports: [ErrorStateComponent],
|
|
1413
|
+
template: \`
|
|
1414
|
+
<div
|
|
1415
|
+
class="rounded-lg border border-border bg-background-welcome dark:border-white/10 dark:bg-ink-950"
|
|
1416
|
+
>
|
|
1417
|
+
<africanies-error-state
|
|
1418
|
+
[message]="errorMessage()"
|
|
1419
|
+
(retry)="refetch()"
|
|
1420
|
+
/>
|
|
1421
|
+
</div>
|
|
1422
|
+
\`,
|
|
1423
|
+
})
|
|
1424
|
+
export class ErrorDemoComponent {
|
|
1425
|
+
// In production: map injectQuery() / getResource() error signal to this string.
|
|
1426
|
+
protected readonly errorMessage = signal(
|
|
1427
|
+
'Failed to load shipments. Check your connection and try again.',
|
|
1428
|
+
);
|
|
1429
|
+
|
|
1430
|
+
protected refetch(): void {
|
|
1431
|
+
// query.refetch() or resource.reload() \u2014 same handler AsyncState uses via (retry).
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
`,Ar=`
|
|
1435
|
+
// Load succeeded but nothing matched. Retry often means resetFilters() or refetch.
|
|
1436
|
+
// Prefer AsyncState on list pages \u2014 it picks EmptyState when data is [].
|
|
1437
|
+
|
|
1438
|
+
import { Component, signal } from '@angular/core';
|
|
1439
|
+
import { EmptyStateComponent } from '@africanies/africanies-ui';
|
|
1440
|
+
|
|
1441
|
+
@Component({
|
|
1442
|
+
selector: 'app-empty-demo',
|
|
1443
|
+
standalone: true,
|
|
1444
|
+
imports: [EmptyStateComponent],
|
|
1445
|
+
template: \`
|
|
1446
|
+
<africanies-empty-state
|
|
1447
|
+
[message]="message()"
|
|
1448
|
+
(retry)="resetFilters()"
|
|
1449
|
+
/>
|
|
1450
|
+
\`,
|
|
1451
|
+
})
|
|
1452
|
+
export class EmptyDemoComponent {
|
|
1453
|
+
protected readonly message = signal('No shipments match your filters.');
|
|
1454
|
+
|
|
1455
|
+
protected resetFilters(): void {
|
|
1456
|
+
// Clear facet signals \u2192 triggers list refetch with default params.
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
`,kr=`
|
|
1460
|
+
// Non-blocking retry pill \u2014 stale data stays visible underneath.
|
|
1461
|
+
// AsyncState uses this automatically when data exists but refresh failed.
|
|
1462
|
+
|
|
1463
|
+
import { Component, signal } from '@angular/core';
|
|
1464
|
+
import { ErrorIndicatorComponent } from '@africanies/africanies-ui';
|
|
1465
|
+
|
|
1466
|
+
@Component({
|
|
1467
|
+
selector: 'app-stale-data-panel',
|
|
1468
|
+
standalone: true,
|
|
1469
|
+
imports: [ErrorIndicatorComponent],
|
|
1470
|
+
template: \`
|
|
1471
|
+
<div class="flex flex-col gap-3 rounded-xl border border-border p-4 dark:border-white/10">
|
|
1472
|
+
<div class="flex justify-end">
|
|
1473
|
+
<africanies-error-indicator
|
|
1474
|
+
class="max-w-[min(100%,20rem)]"
|
|
1475
|
+
error="Failed to fetch the most recent data."
|
|
1476
|
+
retryText="Refresh"
|
|
1477
|
+
[refreshing]="isRefreshing()"
|
|
1478
|
+
[refreshingText]="isRefreshing() ? 'Refreshing...' : 'Refresh'"
|
|
1479
|
+
(retry)="refetch()"
|
|
1480
|
+
/>
|
|
1481
|
+
</div>
|
|
1482
|
+
|
|
1483
|
+
<!-- Existing table / cards stay mounted -->
|
|
1484
|
+
<ng-content />
|
|
1485
|
+
</div>
|
|
1486
|
+
\`,
|
|
1487
|
+
})
|
|
1488
|
+
export class StaleDataPanelComponent {
|
|
1489
|
+
protected readonly isRefreshing = signal(false);
|
|
1490
|
+
|
|
1491
|
+
protected refetch(): void {
|
|
1492
|
+
this.isRefreshing.set(true);
|
|
1493
|
+
// query.refetch().finally(() => this.isRefreshing.set(false));
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// Standalone call sites:
|
|
1498
|
+
// Socket: error="Connection lost" retryText="Reconnect" refreshingText="Connecting..."
|
|
1499
|
+
// Currencies: error="Error fetching currencies" (retryText defaults to "Retry")
|
|
1500
|
+
`,_r=`
|
|
1501
|
+
// Wrap the list (table + pagination). Maps loading / error / empty / success for you.
|
|
1502
|
+
// Background refetch keeps content on screen. Wire your own in-flight cue (e.g. table Refresh). One (retry) \u2192 refetch.
|
|
1503
|
+
|
|
1504
|
+
import { Component, computed, inject, signal } from '@angular/core';
|
|
1505
|
+
import type { AsyncQueryStateModel } from '@africanies/africanies-models';
|
|
1506
|
+
import {
|
|
1507
|
+
AsyncStateComponent,
|
|
1508
|
+
TableComponent,
|
|
1509
|
+
type TableColumn,
|
|
1510
|
+
} from '@africanies/africanies-ui';
|
|
1511
|
+
|
|
1512
|
+
interface Shipment {
|
|
1513
|
+
id: string;
|
|
1514
|
+
route: string;
|
|
1515
|
+
status: string;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
@Component({
|
|
1519
|
+
selector: 'app-shipment-list',
|
|
1520
|
+
standalone: true,
|
|
1521
|
+
imports: [AsyncStateComponent, TableComponent],
|
|
1522
|
+
template: \`
|
|
1523
|
+
<africanies-async-state [state]="state()" (retry)="refetch()">
|
|
1524
|
+
<africanies-table
|
|
1525
|
+
[columns]="columns"
|
|
1526
|
+
[rows]="state().data ?? []"
|
|
1527
|
+
[refreshing]="state().isFetching"
|
|
1528
|
+
/>
|
|
1529
|
+
</africanies-async-state>
|
|
1530
|
+
\`,
|
|
1531
|
+
})
|
|
1532
|
+
export class ShipmentListComponent {
|
|
1533
|
+
// Playground: manual signal. Production: computed from injectQuery().
|
|
1534
|
+
private readonly queryData = signal<Shipment[] | undefined>(undefined);
|
|
1535
|
+
private readonly queryLoading = signal(true);
|
|
1536
|
+
private readonly queryFetching = signal(false);
|
|
1537
|
+
private readonly queryError = signal<string | null>(null);
|
|
1538
|
+
|
|
1539
|
+
protected readonly state = computed((): AsyncQueryStateModel<Shipment[]> => ({
|
|
1540
|
+
data: this.queryData(),
|
|
1541
|
+
isLoading: this.queryLoading(),
|
|
1542
|
+
isFetching: this.queryFetching(),
|
|
1543
|
+
isError: this.queryError() !== null,
|
|
1544
|
+
error: this.queryError(),
|
|
1545
|
+
}));
|
|
1546
|
+
|
|
1547
|
+
protected readonly columns: TableColumn<Shipment>[] = [
|
|
1548
|
+
{ key: 'id', header: 'Reference', sortable: true },
|
|
1549
|
+
{ key: 'route', header: 'Route' },
|
|
1550
|
+
{ key: 'status', header: 'Status', sortable: true },
|
|
1551
|
+
];
|
|
1552
|
+
|
|
1553
|
+
protected refetch(): void {
|
|
1554
|
+
// Production one-liner: this.shipmentsQuery.refetch();
|
|
1555
|
+
this.queryLoading.set(this.queryData() === undefined);
|
|
1556
|
+
this.queryFetching.set(true);
|
|
1557
|
+
this.queryError.set(null);
|
|
1558
|
+
// \u2026 HTTP GET \u2192 on success set queryData + clear flags; on error set queryError.
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// Production mapping (TanStack Query) \u2014 drop into the same component:
|
|
1563
|
+
//
|
|
1564
|
+
// readonly query = injectQuery(() => ({
|
|
1565
|
+
// queryKey: ['shipments', this.filters()],
|
|
1566
|
+
// queryFn: () => this.api.getShipments(this.filters()),
|
|
1567
|
+
// }));
|
|
1568
|
+
//
|
|
1569
|
+
// readonly state = computed<AsyncQueryStateModel<Shipment[]>>(() => ({
|
|
1570
|
+
// data: this.query.data(),
|
|
1571
|
+
// isLoading: this.query.isLoading(),
|
|
1572
|
+
// isFetching: this.query.isFetching(),
|
|
1573
|
+
// isError: this.query.isError(),
|
|
1574
|
+
// error: this.query.error()?.message ?? null,
|
|
1575
|
+
// }));
|
|
1576
|
+
`;var Mr=`
|
|
1577
|
+
// Open the drawer, await the list refetch, then commit state on success.
|
|
1578
|
+
// Host owns FilterStateModel; drawer edits a clone. Needs provideAfricaniesUiOverlays().
|
|
1579
|
+
// Apply writes the query bag to the URL (page resets to 1). Seed from the URL
|
|
1580
|
+
// on first paint with FilterQueryService.read() when those queries are present.
|
|
1581
|
+
|
|
1582
|
+
import { Component, inject, signal } from '@angular/core';
|
|
1583
|
+
import {
|
|
1584
|
+
ButtonComponent,
|
|
1585
|
+
FilterDrawerService,
|
|
1586
|
+
FilterQueryService,
|
|
1587
|
+
emptyFilterState,
|
|
1588
|
+
trackShipmentsFilterConfig,
|
|
1589
|
+
type FilterStateModel,
|
|
1590
|
+
} from '@africanies/africanies-ui';
|
|
1591
|
+
|
|
1592
|
+
@Component({
|
|
1593
|
+
selector: 'app-shipment-list-filters',
|
|
1594
|
+
standalone: true,
|
|
1595
|
+
imports: [ButtonComponent],
|
|
1596
|
+
template: \`
|
|
1597
|
+
<button africanies-button type="button" variant="secondary" (click)="openFilters()">
|
|
1598
|
+
Filters
|
|
1599
|
+
</button>
|
|
1600
|
+
\`,
|
|
1601
|
+
})
|
|
1602
|
+
export class ShipmentListFiltersComponent {
|
|
1603
|
+
private readonly filterDrawer = inject(FilterDrawerService);
|
|
1604
|
+
private readonly filterQuery = inject(FilterQueryService);
|
|
1605
|
+
private readonly http = inject(/* your HttpClient or list service */);
|
|
1606
|
+
private readonly config = trackShipmentsFilterConfig;
|
|
1607
|
+
|
|
1608
|
+
protected readonly state = signal<FilterStateModel>(
|
|
1609
|
+
this.filterQuery.hasParams(this.config)
|
|
1610
|
+
? this.filterQuery.read(this.config)
|
|
1611
|
+
: emptyFilterState(),
|
|
1612
|
+
);
|
|
1613
|
+
|
|
1614
|
+
protected openFilters(): void {
|
|
1615
|
+
this.filterDrawer
|
|
1616
|
+
.open({
|
|
1617
|
+
config: this.config,
|
|
1618
|
+
state: this.state(),
|
|
1619
|
+
title: 'Track shipments',
|
|
1620
|
+
// Stays open until this settles; errors keep it open + show a tip.
|
|
1621
|
+
onApply: ({ params }) => this.http.get('/api/shipments', { params }),
|
|
1622
|
+
})
|
|
1623
|
+
.afterClosed()
|
|
1624
|
+
.subscribe((result) => {
|
|
1625
|
+
if (!result?.applied) {
|
|
1626
|
+
return; // Cancel / backdrop \u2014 leave host state alone
|
|
1627
|
+
}
|
|
1628
|
+
this.state.set(result.state);
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
`,Ir=`
|
|
1633
|
+
// Custom ModuleFilterConfigModel for your list screen.
|
|
1634
|
+
// Omit transport \u2192 defaults to FilterTransport.LegacyParallel (filterColumn CSV).
|
|
1635
|
+
// Set transport: FilterTransport.Named when each field is its own query param.
|
|
1636
|
+
|
|
1637
|
+
import {
|
|
1638
|
+
FilterTransport,
|
|
1639
|
+
type ModuleFilterConfigModel,
|
|
1640
|
+
} from '@africanies/africanies-models';
|
|
1641
|
+
|
|
1642
|
+
export const myOrdersFilterConfig: ModuleFilterConfigModel = {
|
|
1643
|
+
id: 'my-orders',
|
|
1644
|
+
search: {
|
|
1645
|
+
param: 'search',
|
|
1646
|
+
label: 'Order ref',
|
|
1647
|
+
placeholder: 'Search\u2026',
|
|
1648
|
+
},
|
|
1649
|
+
sort: {
|
|
1650
|
+
param: 'order',
|
|
1651
|
+
options: [
|
|
1652
|
+
{ value: 'desc', label: 'Newest first' },
|
|
1653
|
+
{ value: 'asc', label: 'Oldest first' },
|
|
1654
|
+
],
|
|
1655
|
+
},
|
|
1656
|
+
pagination: { pageParam: 'page', sizeParam: 'size' },
|
|
1657
|
+
fields: [
|
|
1658
|
+
{
|
|
1659
|
+
key: 'status',
|
|
1660
|
+
label: 'Status',
|
|
1661
|
+
type: 'enum',
|
|
1662
|
+
options: [
|
|
1663
|
+
{ value: 'open', label: 'Open', color: '#3B82F6' },
|
|
1664
|
+
{ value: 'closed', label: 'Closed', color: '#25945c' },
|
|
1665
|
+
],
|
|
1666
|
+
},
|
|
1667
|
+
{
|
|
1668
|
+
key: 'warehouse_id',
|
|
1669
|
+
label: 'Warehouse',
|
|
1670
|
+
type: 'select',
|
|
1671
|
+
optionsSource: 'warehouses',
|
|
1672
|
+
placeholder: 'All warehouses',
|
|
1673
|
+
},
|
|
1674
|
+
],
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
// Newer API \u2014 explicit named transport:
|
|
1678
|
+
export const myClaimsFilterConfig: ModuleFilterConfigModel = {
|
|
1679
|
+
id: 'claims',
|
|
1680
|
+
transport: FilterTransport.Named,
|
|
1681
|
+
search: { param: 'search', label: 'Search' },
|
|
1682
|
+
fields: [
|
|
1683
|
+
{
|
|
1684
|
+
key: 'claim_status',
|
|
1685
|
+
label: 'Claim status',
|
|
1686
|
+
type: 'enum',
|
|
1687
|
+
options: [{ value: 'open', label: 'Open' }],
|
|
1688
|
+
},
|
|
1689
|
+
],
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1692
|
+
// Pass to the drawer (same UI for both transports):
|
|
1693
|
+
// filterDrawer.open({ config: myOrdersFilterConfig, state, onApply: \u2026 });
|
|
1694
|
+
`,Nr=`
|
|
1695
|
+
// Full seed example (update-shipments) \u2014 field \`type\` picks the control.
|
|
1696
|
+
// See FILTERS_CUSTOM_CONFIG for a minimal custom module.
|
|
1697
|
+
|
|
1698
|
+
import { FilterTransport, type ModuleFilterConfigModel } from '@africanies/africanies-models';
|
|
1699
|
+
|
|
1700
|
+
export const updateShipmentsFilterConfig: ModuleFilterConfigModel = {
|
|
1701
|
+
id: 'update-shipments',
|
|
1702
|
+
route: ['portal', 'shipment', 'update-shipments'],
|
|
1703
|
+
// transport omitted \u2192 FilterTransport.LegacyParallel
|
|
1704
|
+
pagination: { pageParam: 'page', sizeParam: 'size' },
|
|
1705
|
+
search: {
|
|
1706
|
+
param: 'search',
|
|
1707
|
+
label: 'Shipment ID',
|
|
1708
|
+
placeholder: 'Search',
|
|
1709
|
+
},
|
|
1710
|
+
date: {
|
|
1711
|
+
rangeParams: { from: 'from', to: 'to' },
|
|
1712
|
+
fieldParam: 'date',
|
|
1713
|
+
fields: [
|
|
1714
|
+
{ value: 'created_at', label: 'Date Created' },
|
|
1715
|
+
{ value: 'paid_at', label: 'Date of Payment' },
|
|
1716
|
+
{ value: 'packaged_at', label: 'Date Packaged' },
|
|
1717
|
+
{ value: 'shipment_processed_at', label: 'Date Processed' },
|
|
1718
|
+
],
|
|
1719
|
+
},
|
|
1720
|
+
sort: {
|
|
1721
|
+
param: 'order',
|
|
1722
|
+
options: [
|
|
1723
|
+
{ value: 'asc', label: 'Ascending' },
|
|
1724
|
+
{ value: 'desc', label: 'Descending' },
|
|
1725
|
+
],
|
|
1726
|
+
},
|
|
1727
|
+
fields: [
|
|
1728
|
+
{
|
|
1729
|
+
key: 'payment_status',
|
|
1730
|
+
label: 'Payment Status',
|
|
1731
|
+
type: 'enum',
|
|
1732
|
+
exclusive: true,
|
|
1733
|
+
options: [
|
|
1734
|
+
{ value: 'paid', label: 'Paid', color: '#25945c' },
|
|
1735
|
+
{ value: 'unpaid', label: 'Unpaid', color: '#f48220' },
|
|
1736
|
+
],
|
|
1737
|
+
},
|
|
1738
|
+
{
|
|
1739
|
+
key: 'shipment_status',
|
|
1740
|
+
label: 'Shipment Status',
|
|
1741
|
+
type: 'enum',
|
|
1742
|
+
exclusive: true,
|
|
1743
|
+
options: [
|
|
1744
|
+
{ value: 'pending', label: 'Pending', color: '#DBB316' },
|
|
1745
|
+
{ value: 'in-process', label: 'In Process', color: '#3B82F6' },
|
|
1746
|
+
{ value: 'completed', label: 'Completed', color: '#25945c' },
|
|
1747
|
+
],
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
key: 'type',
|
|
1751
|
+
label: 'Shipment Type',
|
|
1752
|
+
type: 'enum',
|
|
1753
|
+
exclusive: true,
|
|
1754
|
+
options: [
|
|
1755
|
+
{ value: 'shipment', label: 'Shipment', color: '#3B82F6' },
|
|
1756
|
+
{ value: 'etw_shipment', label: 'ETW Shipment', color: '#8B5CF6' },
|
|
1757
|
+
],
|
|
1758
|
+
},
|
|
1759
|
+
{
|
|
1760
|
+
key: 'shipment_method_id',
|
|
1761
|
+
label: 'Shipment Carrier',
|
|
1762
|
+
type: 'select',
|
|
1763
|
+
optionsSource: 'shipmentMethods',
|
|
1764
|
+
placeholder: 'Shipment Methods',
|
|
1765
|
+
},
|
|
1766
|
+
{
|
|
1767
|
+
key: 'type_of_user',
|
|
1768
|
+
label: 'User Type',
|
|
1769
|
+
type: 'select',
|
|
1770
|
+
optionsSource: 'static',
|
|
1771
|
+
options: [
|
|
1772
|
+
{ value: 'individual', label: 'Individual' },
|
|
1773
|
+
{ value: 'business', label: 'Business' },
|
|
1774
|
+
],
|
|
1775
|
+
placeholder: 'User Type',
|
|
1776
|
+
},
|
|
1777
|
+
{
|
|
1778
|
+
key: 'warehouse_id',
|
|
1779
|
+
label: 'Warehouse',
|
|
1780
|
+
type: 'select',
|
|
1781
|
+
optionsSource: 'warehouses',
|
|
1782
|
+
placeholder: 'Warehouse',
|
|
1783
|
+
},
|
|
1784
|
+
{
|
|
1785
|
+
key: 'shipment_manifest_id',
|
|
1786
|
+
label: 'Shipment Manifest',
|
|
1787
|
+
type: 'select',
|
|
1788
|
+
optionsSource: 'shipmentManifests',
|
|
1789
|
+
placeholder: 'Shipment Manifest',
|
|
1790
|
+
},
|
|
1791
|
+
{
|
|
1792
|
+
key: 'api_request',
|
|
1793
|
+
label: 'API Request',
|
|
1794
|
+
type: 'boolean',
|
|
1795
|
+
options: [
|
|
1796
|
+
{ value: '1', label: 'Yes' },
|
|
1797
|
+
{ value: '0', label: 'No' },
|
|
1798
|
+
],
|
|
1799
|
+
},
|
|
1800
|
+
{
|
|
1801
|
+
key: 'is_insured',
|
|
1802
|
+
label: 'Insured',
|
|
1803
|
+
type: 'boolean',
|
|
1804
|
+
options: [
|
|
1805
|
+
{ value: '1', label: 'Yes' },
|
|
1806
|
+
{ value: '0', label: 'No' },
|
|
1807
|
+
],
|
|
1808
|
+
},
|
|
1809
|
+
],
|
|
1810
|
+
};
|
|
1811
|
+
`,Rr=`
|
|
1812
|
+
// Status chips. Optional \`color\` tints the selected chip; idle stays neutral.
|
|
1813
|
+
// color is UI-only \u2014 never sent in params.
|
|
1814
|
+
|
|
1815
|
+
{
|
|
1816
|
+
key: 'shipment_status',
|
|
1817
|
+
label: 'Shipment Status',
|
|
1818
|
+
type: 'enum',
|
|
1819
|
+
exclusive: true, // one value at a time
|
|
1820
|
+
options: [
|
|
1821
|
+
{ value: 'pending', label: 'Pending', color: '#DBB316' },
|
|
1822
|
+
{ value: 'in-process', label: 'In Process', color: '#3B82F6' },
|
|
1823
|
+
{ value: 'completed', label: 'Completed', color: '#25945c' },
|
|
1824
|
+
],
|
|
1825
|
+
}
|
|
1826
|
+
`,Or=`
|
|
1827
|
+
// \u201CFilter by\u201D lists every config.fields entry; picking one reveals its control.
|
|
1828
|
+
// Deselect clears that section\u2019s value on Apply. Keep field.key stable.
|
|
1829
|
+
|
|
1830
|
+
// User picks \u201CPayment Status\u201D + \u201CWarehouse\u201D in Filter by \u2192
|
|
1831
|
+
// draft.values = { payment_status: 'paid', warehouse_id: '12' }
|
|
1832
|
+
`,Dr=`
|
|
1833
|
+
// Date column select + From / To. Clear resets only those three.
|
|
1834
|
+
// To can\u2019t be before From \u2014 moving From past To clears To.
|
|
1835
|
+
|
|
1836
|
+
date: {
|
|
1837
|
+
rangeParams: { from: 'from', to: 'to' },
|
|
1838
|
+
fieldParam: 'date',
|
|
1839
|
+
fields: [
|
|
1840
|
+
{ value: 'created_at', label: 'Date Created' },
|
|
1841
|
+
{ value: 'paid_at', label: 'Date of Payment' },
|
|
1842
|
+
],
|
|
1843
|
+
},
|
|
1844
|
+
|
|
1845
|
+
// Example state \u2192 params
|
|
1846
|
+
// { date: 'created_at', from: '2026-01-01', to: '2026-01-31' }
|
|
1847
|
+
`,Pr=`
|
|
1848
|
+
// Laravel-style: filterColumn + filterValue CSVs by index, plus search/date/sort/page.
|
|
1849
|
+
// Keep values keyed by field.key; only call toFilterParams at the URL/HTTP boundary.
|
|
1850
|
+
|
|
1851
|
+
import {
|
|
1852
|
+
fromFilterParams,
|
|
1853
|
+
toFilterParams,
|
|
1854
|
+
trackShipmentsFilterConfig,
|
|
1855
|
+
type FilterStateModel,
|
|
1856
|
+
} from '@africanies/africanies-models';
|
|
1857
|
+
|
|
1858
|
+
const state: FilterStateModel = {
|
|
1859
|
+
search: 'SFN-1042',
|
|
1860
|
+
date: 'created_at',
|
|
1861
|
+
from: '2026-01-01',
|
|
1862
|
+
to: '2026-01-31',
|
|
1863
|
+
order: 'desc',
|
|
1864
|
+
page: 1,
|
|
1865
|
+
size: 15,
|
|
1866
|
+
values: {
|
|
1867
|
+
payment_status: 'paid',
|
|
1868
|
+
shipment_status: 'pending',
|
|
1869
|
+
tracking_number: 'TN-9',
|
|
1870
|
+
},
|
|
1871
|
+
};
|
|
1872
|
+
|
|
1873
|
+
const params = toFilterParams(state, trackShipmentsFilterConfig);
|
|
1874
|
+
// \u2192 {
|
|
1875
|
+
// search: 'SFN-1042',
|
|
1876
|
+
// date: 'created_at',
|
|
1877
|
+
// from: '2026-01-01',
|
|
1878
|
+
// to: '2026-01-31',
|
|
1879
|
+
// order: 'desc',
|
|
1880
|
+
// page: 1,
|
|
1881
|
+
// size: 15,
|
|
1882
|
+
// filterColumn: 'payment_status,shipment_status,tracking_number',
|
|
1883
|
+
// filterValue: 'paid,pending,TN-9',
|
|
1884
|
+
// }
|
|
1885
|
+
|
|
1886
|
+
// Cold load from router queryParams:
|
|
1887
|
+
const restored = fromFilterParams(params, trackShipmentsFilterConfig);
|
|
1888
|
+
// restored.values.payment_status === 'paid'
|
|
1889
|
+
`,Lr=`
|
|
1890
|
+
// Newer endpoints: each filter is its own query key (no filterColumn CSVs).
|
|
1891
|
+
// Same drawer + FilterStateModel; set transport: FilterTransport.Named.
|
|
1892
|
+
|
|
1893
|
+
import {
|
|
1894
|
+
FilterTransport,
|
|
1895
|
+
shipmentTrackingItemFilterConfig,
|
|
1896
|
+
toFilterParams,
|
|
1897
|
+
type FilterStateModel,
|
|
1898
|
+
} from '@africanies/africanies-models';
|
|
1899
|
+
|
|
1900
|
+
const state: FilterStateModel = {
|
|
1901
|
+
search: 'AWB',
|
|
1902
|
+
values: { claim_status: 'open' },
|
|
1903
|
+
};
|
|
1904
|
+
|
|
1905
|
+
toFilterParams(state, shipmentTrackingItemFilterConfig);
|
|
1906
|
+
// \u2192 { search: 'AWB', claim_status: 'open' }
|
|
1907
|
+
// (no filterColumn / filterValue)
|
|
1908
|
+
`,Fr=`// optionsSource catalogs load lazily inside the filter drawer when the user
|
|
1909
|
+
// adds the field via Filter by. HTTP errors toast via httpToasts; the select
|
|
1910
|
+
// shows a field error. Pass host optionLists for sources without an SDK service.
|
|
1911
|
+
|
|
1912
|
+
import { Component, inject, signal } from '@angular/core';
|
|
1913
|
+
import {
|
|
1914
|
+
emptyFilterState,
|
|
1915
|
+
updateShipmentsFilterConfig,
|
|
1916
|
+
type FilterStateModel,
|
|
1917
|
+
} from '@africanies/africanies-models';
|
|
1918
|
+
import { FilterDrawerService } from '@africanies/africanies-ui';
|
|
1919
|
+
|
|
1920
|
+
@Component({
|
|
1921
|
+
selector: 'app-update-shipment-filters',
|
|
1922
|
+
standalone: true,
|
|
1923
|
+
template: \`<button type="button" (click)="open()">Filters</button>\`,
|
|
1924
|
+
})
|
|
1925
|
+
export class UpdateShipmentFiltersComponent {
|
|
1926
|
+
private readonly filterDrawer = inject(FilterDrawerService);
|
|
1927
|
+
|
|
1928
|
+
protected readonly state = signal<FilterStateModel>(emptyFilterState());
|
|
1929
|
+
|
|
1930
|
+
protected open(): void {
|
|
1931
|
+
this.filterDrawer
|
|
1932
|
+
.open({
|
|
1933
|
+
config: updateShipmentsFilterConfig,
|
|
1934
|
+
state: this.state(),
|
|
1935
|
+
optionLists: {
|
|
1936
|
+
// No built-in SDK service yet \u2014 host supplies manifests
|
|
1937
|
+
shipment_manifest_id: [{ value: '100', label: 'Manifest A' }],
|
|
1938
|
+
},
|
|
1939
|
+
onApply: ({ params }) => this.listApi.fetch(params),
|
|
1940
|
+
})
|
|
1941
|
+
.afterClosed()
|
|
1942
|
+
.subscribe((result) => {
|
|
1943
|
+
if (result?.applied) {
|
|
1944
|
+
this.state.set(result.state);
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
`,Br=`
|
|
1950
|
+
// First paint: rebuild FilterStateModel from the URL so shared links restore filters.
|
|
1951
|
+
// FilterQueryService.read() is a no-op into empty state when those queries are absent.
|
|
1952
|
+
// Apply and africanies-pagination write the same keys back (\`page\`, \`size\`, filters).
|
|
1953
|
+
|
|
1954
|
+
import { inject, signal } from '@angular/core';
|
|
1955
|
+
import {
|
|
1956
|
+
FilterQueryService,
|
|
1957
|
+
emptyFilterState,
|
|
1958
|
+
trackShipmentsFilterConfig,
|
|
1959
|
+
type FilterStateModel,
|
|
1960
|
+
} from '@africanies/africanies-ui';
|
|
1961
|
+
|
|
1962
|
+
export class ListPage {
|
|
1963
|
+
private readonly filterQuery = inject(FilterQueryService);
|
|
1964
|
+
private readonly config = trackShipmentsFilterConfig;
|
|
1965
|
+
|
|
1966
|
+
protected readonly state = signal<FilterStateModel>(
|
|
1967
|
+
this.filterQuery.hasParams(this.config)
|
|
1968
|
+
? this.filterQuery.read(this.config)
|
|
1969
|
+
: emptyFilterState(),
|
|
1970
|
+
);
|
|
1971
|
+
|
|
1972
|
+
protected readonly page = signal(this.state().page ?? 1);
|
|
1973
|
+
protected readonly size = signal(this.state().size ?? 15);
|
|
1974
|
+
}
|
|
1975
|
+
`;var jr=`
|
|
1976
|
+
// Text fields: label + optional hint/error. Bind with [(value)].
|
|
1977
|
+
// Prefix/suffix slots use attribute selectors (prefix, suffix).
|
|
1978
|
+
|
|
1979
|
+
import { Component, signal } from '@angular/core';
|
|
1980
|
+
import { AfricaniesIconComponent } from '@africanies/africanies-icons';
|
|
1981
|
+
import { TextInputComponent } from '@africanies/africanies-ui';
|
|
1982
|
+
|
|
1983
|
+
@Component({
|
|
1984
|
+
selector: 'app-shipment-reference-form',
|
|
1985
|
+
standalone: true,
|
|
1986
|
+
imports: [TextInputComponent, AfricaniesIconComponent],
|
|
1987
|
+
template: \`
|
|
1988
|
+
<div class="grid gap-5 md:grid-cols-2">
|
|
1989
|
+
<africanies-text-input
|
|
1990
|
+
label="Tracking number"
|
|
1991
|
+
hint="As printed on the airway bill"
|
|
1992
|
+
placeholder="e.g. AWB-12345"
|
|
1993
|
+
[(value)]="tracking"
|
|
1994
|
+
/>
|
|
1995
|
+
|
|
1996
|
+
<africanies-text-input label="Reference" placeholder="Optional internal ref" [(value)]="reference">
|
|
1997
|
+
<africanies-icon prefix name="anchor" [size]="16" />
|
|
1998
|
+
</africanies-text-input>
|
|
1999
|
+
|
|
2000
|
+
<africanies-text-input
|
|
2001
|
+
label="Consignee email"
|
|
2002
|
+
error="Enter a valid email address"
|
|
2003
|
+
[(value)]="email"
|
|
2004
|
+
/>
|
|
2005
|
+
|
|
2006
|
+
<africanies-text-input label="Locked field" [disabled]="true" [(value)]="locked" />
|
|
2007
|
+
</div>
|
|
2008
|
+
\`,
|
|
2009
|
+
})
|
|
2010
|
+
export class ShipmentReferenceFormComponent {
|
|
2011
|
+
protected readonly tracking = signal('');
|
|
2012
|
+
protected readonly reference = signal('');
|
|
2013
|
+
protected readonly email = signal('not-an-email');
|
|
2014
|
+
protected readonly locked = signal('WH-LOCKED-01');
|
|
2015
|
+
}
|
|
2016
|
+
`,zr=`
|
|
2017
|
+
// Same label / hint / error pattern as text inputs, just multi-line.
|
|
2018
|
+
|
|
2019
|
+
import { Component, signal } from '@angular/core';
|
|
2020
|
+
import { TextareaComponent } from '@africanies/africanies-ui';
|
|
2021
|
+
|
|
2022
|
+
@Component({
|
|
2023
|
+
selector: 'app-shipment-instructions-form',
|
|
2024
|
+
standalone: true,
|
|
2025
|
+
imports: [TextareaComponent],
|
|
2026
|
+
template: \`
|
|
2027
|
+
<div class="grid gap-5 md:grid-cols-2">
|
|
2028
|
+
<africanies-textarea
|
|
2029
|
+
label="Special instructions"
|
|
2030
|
+
hint="Visible to warehouse staff"
|
|
2031
|
+
placeholder="Fragile \u2014 keep upright"
|
|
2032
|
+
[(value)]="instructions"
|
|
2033
|
+
/>
|
|
2034
|
+
|
|
2035
|
+
<africanies-textarea
|
|
2036
|
+
label="Notes"
|
|
2037
|
+
error="Notes cannot exceed 500 characters"
|
|
2038
|
+
[(value)]="notesError"
|
|
2039
|
+
/>
|
|
2040
|
+
</div>
|
|
2041
|
+
\`,
|
|
2042
|
+
})
|
|
2043
|
+
export class ShipmentInstructionsFormComponent {
|
|
2044
|
+
protected readonly instructions = signal('');
|
|
2045
|
+
protected readonly notesError = signal('');
|
|
2046
|
+
}
|
|
2047
|
+
`,Gr=`
|
|
2048
|
+
// Bound value is number | null. Prefix/suffix are display chrome only \u2014
|
|
2049
|
+
// commas in the UI are cosmetic; send the numeric value to the API.
|
|
2050
|
+
|
|
2051
|
+
import { DecimalPipe } from '@angular/common';
|
|
2052
|
+
import { Component, signal } from '@angular/core';
|
|
2053
|
+
import { NumberInputComponent } from '@africanies/africanies-ui';
|
|
2054
|
+
|
|
2055
|
+
@Component({
|
|
2056
|
+
selector: 'app-shipment-value-form',
|
|
2057
|
+
standalone: true,
|
|
2058
|
+
imports: [DecimalPipe, NumberInputComponent],
|
|
2059
|
+
template: \`
|
|
2060
|
+
<div class="grid gap-5 md:grid-cols-2">
|
|
2061
|
+
<africanies-number-input label="Declared value (USD)" [(value)]="amount">
|
|
2062
|
+
<span prefix>$</span>
|
|
2063
|
+
</africanies-number-input>
|
|
2064
|
+
|
|
2065
|
+
<africanies-number-input label="Weight" hint="Kilograms" [(value)]="weight">
|
|
2066
|
+
<span suffix>kg</span>
|
|
2067
|
+
</africanies-number-input>
|
|
2068
|
+
</div>
|
|
2069
|
+
|
|
2070
|
+
<p class="text-body-sm">
|
|
2071
|
+
Stored amount:
|
|
2072
|
+
{{ amount() == null ? '\u2014' : (amount()! | number: '1.0-6') }}
|
|
2073
|
+
</p>
|
|
2074
|
+
\`,
|
|
2075
|
+
})
|
|
2076
|
+
export class ShipmentValueFormComponent {
|
|
2077
|
+
protected readonly amount = signal<number | null>(12500);
|
|
2078
|
+
protected readonly weight = signal<number | null>(24.5);
|
|
2079
|
+
}
|
|
2080
|
+
`,Vr=`
|
|
2081
|
+
// Single/multi select binds [(selected)]. [searchable] for long lists.
|
|
2082
|
+
// allowFreeText = inline tags; [create] opens a modal (needs provideAfricaniesUiOverlays()).
|
|
2083
|
+
|
|
2084
|
+
import { Component, signal } from '@angular/core';
|
|
2085
|
+
import { AfricaniesIconComponent } from '@africanies/africanies-icons';
|
|
2086
|
+
import {
|
|
2087
|
+
SelectComponent,
|
|
2088
|
+
type SelectCreateConfig,
|
|
2089
|
+
type SelectOption,
|
|
2090
|
+
} from '@africanies/africanies-ui';
|
|
2091
|
+
|
|
2092
|
+
// Example modal close payload \u2014 your modal component returns this shape.
|
|
2093
|
+
interface WarehouseCreateResult {
|
|
2094
|
+
id: string;
|
|
2095
|
+
name: string;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
@Component({
|
|
2099
|
+
selector: 'app-shipment-routing-form',
|
|
2100
|
+
standalone: true,
|
|
2101
|
+
imports: [SelectComponent, AfricaniesIconComponent],
|
|
2102
|
+
template: \`
|
|
2103
|
+
<div class="grid gap-5 md:grid-cols-2">
|
|
2104
|
+
<africanies-select
|
|
2105
|
+
label="Warehouse"
|
|
2106
|
+
[options]="warehouses()"
|
|
2107
|
+
[searchable]="true"
|
|
2108
|
+
[(selected)]="selectedWarehouse"
|
|
2109
|
+
>
|
|
2110
|
+
<africanies-icon prefix name="warehouse" [size]="16" />
|
|
2111
|
+
</africanies-select>
|
|
2112
|
+
|
|
2113
|
+
<africanies-select
|
|
2114
|
+
label="Tags"
|
|
2115
|
+
[searchable]="true"
|
|
2116
|
+
[allowFreeText]="true"
|
|
2117
|
+
[multiple]="true"
|
|
2118
|
+
[maxSelected]="4"
|
|
2119
|
+
[(options)]="tags"
|
|
2120
|
+
[(selected)]="selectedTags"
|
|
2121
|
+
/>
|
|
2122
|
+
|
|
2123
|
+
<africanies-select
|
|
2124
|
+
label="Incoterm"
|
|
2125
|
+
hint="Plain list \u2014 searchable off"
|
|
2126
|
+
[options]="incoterms"
|
|
2127
|
+
[(selected)]="selectedIncoterm"
|
|
2128
|
+
/>
|
|
2129
|
+
|
|
2130
|
+
<africanies-select
|
|
2131
|
+
label="Carrier"
|
|
2132
|
+
hint="Create opens a modal \u2014 requires provideAfricaniesUiOverlays()"
|
|
2133
|
+
[options]="carriers()"
|
|
2134
|
+
[searchable]="true"
|
|
2135
|
+
[create]="warehouseCreate"
|
|
2136
|
+
[(options)]="carriers"
|
|
2137
|
+
[(selected)]="selectedCarrier"
|
|
2138
|
+
>
|
|
2139
|
+
<africanies-icon prefix name="airplane" [size]="16" />
|
|
2140
|
+
</africanies-select>
|
|
2141
|
+
</div>
|
|
2142
|
+
\`,
|
|
2143
|
+
})
|
|
2144
|
+
export class ShipmentRoutingFormComponent {
|
|
2145
|
+
protected readonly warehouses = signal<SelectOption<string>[]>([
|
|
2146
|
+
{ label: 'Lagos Hub', value: 'los', prefix: 'warehouse', suffix: 'globe' },
|
|
2147
|
+
{ label: 'Accra Depot', value: 'acc', prefix: 'warehouse' },
|
|
2148
|
+
]);
|
|
2149
|
+
|
|
2150
|
+
protected readonly selectedWarehouse = signal<SelectOption<string> | null>(null);
|
|
2151
|
+
|
|
2152
|
+
protected readonly tags = signal<SelectOption<string>[]>([
|
|
2153
|
+
{ label: 'Fragile', value: 'fragile', prefix: 'warning' },
|
|
2154
|
+
{ label: 'Priority', value: 'priority', prefix: 'alarm' },
|
|
2155
|
+
]);
|
|
2156
|
+
protected readonly selectedTags = signal<SelectOption<string>[]>([]);
|
|
2157
|
+
|
|
2158
|
+
protected readonly incoterms: SelectOption<string>[] = [
|
|
2159
|
+
{ label: 'DDP', value: 'ddp' },
|
|
2160
|
+
{ label: 'DAP', value: 'dap' },
|
|
2161
|
+
{ label: 'EXW', value: 'exw' },
|
|
2162
|
+
];
|
|
2163
|
+
protected readonly selectedIncoterm = signal<SelectOption<string> | null>(null);
|
|
2164
|
+
|
|
2165
|
+
protected readonly carriers = signal<SelectOption<string>[]>([
|
|
2166
|
+
{ label: 'DHL Express', value: 'dhl', prefix: 'airplane' },
|
|
2167
|
+
{ label: 'FedEx', value: 'fedex', prefix: 'airplane' },
|
|
2168
|
+
]);
|
|
2169
|
+
protected readonly selectedCarrier = signal<SelectOption<string> | null>(null);
|
|
2170
|
+
|
|
2171
|
+
// Modal-backed create \u2014 component is your standalone creation form.
|
|
2172
|
+
protected readonly warehouseCreate: SelectCreateConfig<WarehouseCreateResult, string> = {
|
|
2173
|
+
label: 'Add warehouse',
|
|
2174
|
+
component: WarehouseCreateModalComponent,
|
|
2175
|
+
data: { region: 'west-africa' },
|
|
2176
|
+
mapResult: (result) => ({
|
|
2177
|
+
label: result.name,
|
|
2178
|
+
value: result.id,
|
|
2179
|
+
prefix: 'warehouse',
|
|
2180
|
+
}),
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// Stub \u2014 replace with your real modal panel opened via ModalService.
|
|
2185
|
+
@Component({ selector: 'app-warehouse-create-modal', standalone: true, template: '' })
|
|
2186
|
+
export class WarehouseCreateModalComponent {}
|
|
2187
|
+
|
|
2188
|
+
// app.config.ts \u2014 required once per app when any select uses [create]:
|
|
2189
|
+
//
|
|
2190
|
+
// import { provideAfricaniesUiOverlays } from '@africanies/africanies-ui';
|
|
2191
|
+
//
|
|
2192
|
+
// export const appConfig: ApplicationConfig = {
|
|
2193
|
+
// providers: [provideAfricaniesUiOverlays(), /* \u2026 */],
|
|
2194
|
+
// };
|
|
2195
|
+
`,Hr=`
|
|
2196
|
+
// Checkbox / toggle for booleans; radio for exactly-one-of. All use [(value)].
|
|
2197
|
+
|
|
2198
|
+
import { Component, signal } from '@angular/core';
|
|
2199
|
+
import {
|
|
2200
|
+
CheckboxComponent,
|
|
2201
|
+
RadioComponent,
|
|
2202
|
+
ToggleComponent,
|
|
2203
|
+
type RadioOption,
|
|
2204
|
+
} from '@africanies/africanies-ui';
|
|
2205
|
+
|
|
2206
|
+
@Component({
|
|
2207
|
+
selector: 'app-shipment-options-form',
|
|
2208
|
+
standalone: true,
|
|
2209
|
+
imports: [CheckboxComponent, ToggleComponent, RadioComponent],
|
|
2210
|
+
template: \`
|
|
2211
|
+
<div class="grid gap-6 md:grid-cols-2">
|
|
2212
|
+
<div class="flex flex-col gap-4">
|
|
2213
|
+
<africanies-checkbox label="Require signature on delivery" [(value)]="signature" />
|
|
2214
|
+
|
|
2215
|
+
<africanies-checkbox
|
|
2216
|
+
label="Insure this shipment"
|
|
2217
|
+
hint="Adds 0.8% of declared value"
|
|
2218
|
+
[(value)]="insured"
|
|
2219
|
+
/>
|
|
2220
|
+
|
|
2221
|
+
<africanies-checkbox
|
|
2222
|
+
label="Accept terms"
|
|
2223
|
+
error="You must accept the terms"
|
|
2224
|
+
[(value)]="terms"
|
|
2225
|
+
/>
|
|
2226
|
+
|
|
2227
|
+
<africanies-toggle label="Notify consignee by SMS" [(value)]="smsNotify" />
|
|
2228
|
+
<africanies-toggle label="Hold at depot" [(value)]="holdAtDepot" />
|
|
2229
|
+
<africanies-toggle
|
|
2230
|
+
label="Require customs hold"
|
|
2231
|
+
hint="Waits for the API \u2014 the switch does not flip until the save succeeds."
|
|
2232
|
+
[value]="customsHold()"
|
|
2233
|
+
[loading]="customsHoldSaving()"
|
|
2234
|
+
(valueChange)="saveCustomsHold($event)"
|
|
2235
|
+
/>
|
|
2236
|
+
</div>
|
|
2237
|
+
|
|
2238
|
+
<africanies-radio
|
|
2239
|
+
label="Service level"
|
|
2240
|
+
[options]="serviceLevels"
|
|
2241
|
+
[(value)]="serviceLevel"
|
|
2242
|
+
/>
|
|
2243
|
+
</div>
|
|
2244
|
+
\`,
|
|
2245
|
+
})
|
|
2246
|
+
export class ShipmentOptionsFormComponent {
|
|
2247
|
+
protected readonly signature = signal(true);
|
|
2248
|
+
protected readonly insured = signal(false);
|
|
2249
|
+
protected readonly terms = signal(false);
|
|
2250
|
+
protected readonly smsNotify = signal(true);
|
|
2251
|
+
protected readonly holdAtDepot = signal(false);
|
|
2252
|
+
protected readonly customsHold = signal(false);
|
|
2253
|
+
protected readonly customsHoldSaving = signal(false);
|
|
2254
|
+
|
|
2255
|
+
protected readonly serviceLevels: RadioOption<string>[] = [
|
|
2256
|
+
{ label: 'Economy', value: 'economy' },
|
|
2257
|
+
{ label: 'Standard', value: 'standard' },
|
|
2258
|
+
{ label: 'Express', value: 'express' },
|
|
2259
|
+
];
|
|
2260
|
+
protected readonly serviceLevel = signal<string | null>('standard');
|
|
2261
|
+
|
|
2262
|
+
protected saveCustomsHold(next: boolean): void {
|
|
2263
|
+
this.customsHoldSaving.set(true);
|
|
2264
|
+
window.setTimeout(() => {
|
|
2265
|
+
this.customsHold.set(next);
|
|
2266
|
+
this.customsHoldSaving.set(false);
|
|
2267
|
+
}, 1200);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
`,Wr=`
|
|
2271
|
+
// Date picker emits YYYY-MM-DD (string | null). Keep that ISO string as-is.
|
|
2272
|
+
|
|
2273
|
+
import { Component, signal } from '@angular/core';
|
|
2274
|
+
import { DatePickerComponent } from '@africanies/africanies-ui';
|
|
2275
|
+
|
|
2276
|
+
@Component({
|
|
2277
|
+
selector: 'app-shipment-schedule-form',
|
|
2278
|
+
standalone: true,
|
|
2279
|
+
imports: [DatePickerComponent],
|
|
2280
|
+
template: \`
|
|
2281
|
+
<div class="grid gap-5 md:grid-cols-2">
|
|
2282
|
+
<africanies-date-picker label="Ready date" hint="Local pickup day" [(value)]="readyDate" />
|
|
2283
|
+
<africanies-date-picker
|
|
2284
|
+
label="Cutoff"
|
|
2285
|
+
error="Cutoff must be after ready date"
|
|
2286
|
+
[(value)]="cutoffDate"
|
|
2287
|
+
/>
|
|
2288
|
+
</div>
|
|
2289
|
+
\`,
|
|
2290
|
+
})
|
|
2291
|
+
export class ShipmentScheduleFormComponent {
|
|
2292
|
+
protected readonly readyDate = signal<string | null>('2026-08-15');
|
|
2293
|
+
protected readonly cutoffDate = signal<string | null>(null);
|
|
2294
|
+
}
|
|
2295
|
+
`,qr=`
|
|
2296
|
+
// Google Places address field. Bootstrap with provideGooglePlaces({ apiKey }).
|
|
2297
|
+
// [(value)] is AddressPlace | null; (placeSelected) fires when a suggestion is chosen.
|
|
2298
|
+
|
|
2299
|
+
import { Component, signal } from '@angular/core';
|
|
2300
|
+
import { AfricaniesIconComponent } from '@africanies/africanies-icons';
|
|
2301
|
+
import {
|
|
2302
|
+
AddressInputComponent,
|
|
2303
|
+
type AddressPlace,
|
|
2304
|
+
provideGooglePlaces,
|
|
2305
|
+
} from '@africanies/africanies-ui';
|
|
2306
|
+
|
|
2307
|
+
// app.config.ts
|
|
2308
|
+
export const appConfig = {
|
|
2309
|
+
providers: [
|
|
2310
|
+
provideGooglePlaces({ apiKey: import.meta.env['NG_APP_GOOGLE_MAPS_KEY'] }),
|
|
2311
|
+
],
|
|
2312
|
+
};
|
|
2313
|
+
|
|
2314
|
+
@Component({
|
|
2315
|
+
selector: 'app-pickup-address-form',
|
|
2316
|
+
standalone: true,
|
|
2317
|
+
imports: [AddressInputComponent, AfricaniesIconComponent],
|
|
2318
|
+
template: \`
|
|
2319
|
+
<africanies-address-input
|
|
2320
|
+
label="Pickup address"
|
|
2321
|
+
hint="Start typing \u2014 pick a Google suggestion"
|
|
2322
|
+
placeholder="Street, city, or landmark"
|
|
2323
|
+
[countries]="['ng', 'gh', 'za']"
|
|
2324
|
+
[(value)]="pickup"
|
|
2325
|
+
(placeSelected)="onPlace($event)"
|
|
2326
|
+
>
|
|
2327
|
+
<africanies-icon prefix name="map-marker" [size]="16" />
|
|
2328
|
+
</africanies-address-input>
|
|
2329
|
+
|
|
2330
|
+
<pre>{{ pickup() | json }}</pre>
|
|
2331
|
+
\`,
|
|
2332
|
+
})
|
|
2333
|
+
export class PickupAddressFormComponent {
|
|
2334
|
+
protected readonly pickup = signal<AddressPlace | null>(null);
|
|
2335
|
+
|
|
2336
|
+
protected onPlace(place: AddressPlace): void {
|
|
2337
|
+
console.log('selected', place);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
`,$r=`
|
|
2341
|
+
// Browse, drag-drop, or camera. You get FileUploadResult[] on (filesSelected) \u2014
|
|
2342
|
+
// upload them yourself. accept filters client-side; mismatches are skipped.
|
|
2343
|
+
// Added files include an underlined View action for a larger preview.
|
|
2344
|
+
|
|
2345
|
+
import { Component, signal } from '@angular/core';
|
|
2346
|
+
import {
|
|
2347
|
+
FileUploadComponent,
|
|
2348
|
+
type FileUploadResult,
|
|
2349
|
+
} from '@africanies/africanies-ui';
|
|
2350
|
+
|
|
2351
|
+
@Component({
|
|
2352
|
+
selector: 'app-kyc-upload-form',
|
|
2353
|
+
standalone: true,
|
|
2354
|
+
imports: [FileUploadComponent],
|
|
2355
|
+
template: \`
|
|
2356
|
+
<africanies-file-upload
|
|
2357
|
+
variant="dropzone"
|
|
2358
|
+
label="Identity document"
|
|
2359
|
+
accept="image/*,.pdf"
|
|
2360
|
+
[allowCamera]="true"
|
|
2361
|
+
[multiple]="true"
|
|
2362
|
+
(filesSelected)="onFiles($event)"
|
|
2363
|
+
/>
|
|
2364
|
+
\`,
|
|
2365
|
+
})
|
|
2366
|
+
export class KycUploadFormComponent {
|
|
2367
|
+
protected readonly files = signal<FileUploadResult[]>([]);
|
|
2368
|
+
|
|
2369
|
+
protected onFiles(next: FileUploadResult[]): void {
|
|
2370
|
+
this.files.set(next);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
`,Kr=`
|
|
2374
|
+
// OTP is one string, not an array. (resend) is your cue to call the API \u2014
|
|
2375
|
+
// the control only runs the cooldown UI. (completed) fires when all digits are in.
|
|
2376
|
+
// variant="masked" hides each digit (PIN-style); the model is still the real code.
|
|
2377
|
+
|
|
2378
|
+
import { Component, signal } from '@angular/core';
|
|
2379
|
+
import { OtpInputComponent } from '@africanies/africanies-ui';
|
|
2380
|
+
|
|
2381
|
+
@Component({
|
|
2382
|
+
selector: 'app-verify-otp-form',
|
|
2383
|
+
standalone: true,
|
|
2384
|
+
imports: [OtpInputComponent],
|
|
2385
|
+
template: \`
|
|
2386
|
+
<africanies-otp-input
|
|
2387
|
+
label="Verification code"
|
|
2388
|
+
hint="Enter the 6-digit code we sent"
|
|
2389
|
+
[resendCooldown]="60"
|
|
2390
|
+
[(value)]="code"
|
|
2391
|
+
(completed)="verify($event)"
|
|
2392
|
+
(resend)="sendAgain()"
|
|
2393
|
+
/>
|
|
2394
|
+
|
|
2395
|
+
<africanies-otp-input
|
|
2396
|
+
label="Transaction PIN"
|
|
2397
|
+
variant="masked"
|
|
2398
|
+
[length]="4"
|
|
2399
|
+
[showResend]="false"
|
|
2400
|
+
[(value)]="pin"
|
|
2401
|
+
(completed)="confirmPin($event)"
|
|
2402
|
+
/>
|
|
2403
|
+
\`,
|
|
2404
|
+
})
|
|
2405
|
+
export class VerifyOtpFormComponent {
|
|
2406
|
+
protected readonly code = signal('');
|
|
2407
|
+
protected readonly pin = signal('');
|
|
2408
|
+
|
|
2409
|
+
protected verify(code: string): void {
|
|
2410
|
+
// POST /auth/verify { code }
|
|
2411
|
+
console.log('OTP complete', code);
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
protected confirmPin(pin: string): void {
|
|
2415
|
+
// POST /wallet/confirm { pin }
|
|
2416
|
+
console.log('PIN complete', pin);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
protected sendAgain(): void {
|
|
2420
|
+
// POST /auth/resend-otp
|
|
2421
|
+
console.log('Resend OTP');
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
`,Yr=`
|
|
2425
|
+
// Same signals you bound with [(value)] / [(selected)] \u2014 read them anywhere.
|
|
2426
|
+
// Call them as functions: tracking(), amount(). Don't forget the ().
|
|
2427
|
+
|
|
2428
|
+
import { DecimalPipe } from '@angular/common';
|
|
2429
|
+
import { Component, signal } from '@angular/core';
|
|
2430
|
+
import { type FileUploadResult, type SelectOption } from '@africanies/africanies-ui';
|
|
2431
|
+
|
|
2432
|
+
@Component({
|
|
2433
|
+
selector: 'app-shipment-summary',
|
|
2434
|
+
standalone: true,
|
|
2435
|
+
imports: [DecimalPipe],
|
|
2436
|
+
template: \`
|
|
2437
|
+
<dl class="m-0 grid gap-2 text-body-sm sm:grid-cols-2">
|
|
2438
|
+
<div>
|
|
2439
|
+
<dt class="text-neutral-600">Tracking</dt>
|
|
2440
|
+
<dd class="m-0 font-medium">{{ tracking() || '\u2014' }}</dd>
|
|
2441
|
+
</div>
|
|
2442
|
+
|
|
2443
|
+
<div>
|
|
2444
|
+
<dt class="text-neutral-600">Amount</dt>
|
|
2445
|
+
<dd class="m-0 font-medium">
|
|
2446
|
+
{{ amount() == null ? '\u2014' : (amount()! | number: '1.0-6') }}
|
|
2447
|
+
</dd>
|
|
2448
|
+
</div>
|
|
2449
|
+
|
|
2450
|
+
<div>
|
|
2451
|
+
<dt class="text-neutral-600">Warehouse</dt>
|
|
2452
|
+
<dd class="m-0 font-medium">{{ selectedWarehouse()?.label ?? '\u2014' }}</dd>
|
|
2453
|
+
</div>
|
|
2454
|
+
|
|
2455
|
+
<div>
|
|
2456
|
+
<dt class="text-neutral-600">Tags</dt>
|
|
2457
|
+
<dd class="m-0 font-medium">{{ tagSummary() }}</dd>
|
|
2458
|
+
</div>
|
|
2459
|
+
|
|
2460
|
+
<div>
|
|
2461
|
+
<dt class="text-neutral-600">Service</dt>
|
|
2462
|
+
<dd class="m-0 font-medium">{{ serviceLevel() ?? '\u2014' }}</dd>
|
|
2463
|
+
</div>
|
|
2464
|
+
|
|
2465
|
+
<div>
|
|
2466
|
+
<dt class="text-neutral-600">Files</dt>
|
|
2467
|
+
<dd class="m-0 font-medium">
|
|
2468
|
+
{{
|
|
2469
|
+
files().length
|
|
2470
|
+
? files()
|
|
2471
|
+
.map((f) => f.file.name)
|
|
2472
|
+
.join(', ')
|
|
2473
|
+
: '\u2014'
|
|
2474
|
+
}}
|
|
2475
|
+
</dd>
|
|
2476
|
+
</div>
|
|
2477
|
+
</dl>
|
|
2478
|
+
\`,
|
|
2479
|
+
})
|
|
2480
|
+
export class ShipmentSummaryComponent {
|
|
2481
|
+
// These signals are the same instances bound in sibling form controls.
|
|
2482
|
+
protected readonly tracking = signal('');
|
|
2483
|
+
protected readonly amount = signal<number | null>(12500);
|
|
2484
|
+
protected readonly selectedWarehouse = signal<SelectOption<string> | null>(null);
|
|
2485
|
+
protected readonly selectedTags = signal<SelectOption<string>[]>([]);
|
|
2486
|
+
protected readonly serviceLevel = signal<string | null>('standard');
|
|
2487
|
+
protected readonly files = signal<FileUploadResult[]>([]);
|
|
2488
|
+
|
|
2489
|
+
protected tagSummary(): string {
|
|
2490
|
+
const selected = this.selectedTags();
|
|
2491
|
+
if (!selected.length) {
|
|
2492
|
+
return '\u2014';
|
|
2493
|
+
}
|
|
2494
|
+
return selected.map((t) => t.label).join(', ');
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
`;var Qr=`// Point Tailwind at the africanies-theme preset and scan @africanies/africanies-ui so its classes aren't purged.
|
|
2498
|
+
// Don't re-declare token colors locally \u2014 extend the preset.
|
|
2499
|
+
|
|
2500
|
+
// tailwind.config.js \u2014 monorepo paths (adjust __dirname to your app root)
|
|
2501
|
+
const path = require('node:path');
|
|
2502
|
+
|
|
2503
|
+
module.exports = {
|
|
2504
|
+
presets: [require('@africanies/africanies-theme/tailwind-preset')],
|
|
2505
|
+
content: [
|
|
2506
|
+
path.join(__dirname, 'src/**/*.{html,ts}'),
|
|
2507
|
+
// Required: scan the UI library so its literal Tailwind classes survive purge.
|
|
2508
|
+
path.join(__dirname, '../../libs/africanies-ui/src/**/*.{html,ts}'),
|
|
2509
|
+
path.join(__dirname, '../../libs/africanies-theme/src/**/*.{html,ts}'),
|
|
2510
|
+
// Published-app equivalent:
|
|
2511
|
+
// './src/**/*.{html,ts}',
|
|
2512
|
+
// './node_modules/@africanies/africanies-ui/**/*.{js,mjs}',
|
|
2513
|
+
],
|
|
2514
|
+
};
|
|
2515
|
+
`,Xr=`// Ink / surface utilities \u2014 use literal class strings (Tailwind can't see dynamic names).
|
|
2516
|
+
|
|
2517
|
+
<div class="bg-ink text-white">Ink surface</div>
|
|
2518
|
+
<div class="bg-ink-brand text-white">Brand ink panel</div>
|
|
2519
|
+
<div class="bg-ink-950 text-white">Near-black panel</div>
|
|
2520
|
+
<div class="bg-black text-white">Pure black</div>
|
|
2521
|
+
<div class="bg-white text-ink">White surface</div>
|
|
2522
|
+
`,Jr=`// Borders, muted copy, page chrome \u2014 stay on the token scale, skip raw hex.
|
|
2523
|
+
|
|
2524
|
+
<div class="rounded-lg border border-border bg-background-welcome p-4">
|
|
2525
|
+
<p class="text-neutral-600">Muted supporting copy</p>
|
|
2526
|
+
<p class="text-ink dark:text-white">Primary body on welcome background</p>
|
|
2527
|
+
</div>
|
|
2528
|
+
|
|
2529
|
+
<span class="text-neutral-400">Disabled / placeholder tone</span>
|
|
2530
|
+
<span class="text-neutral-300">Light divider accent</span>
|
|
2531
|
+
`,eo=`// SFN / export green \u2014 fine for static marketing. Mode-aware chrome? Use ModeColorService.
|
|
2532
|
+
|
|
2533
|
+
<span class="rounded-md bg-export px-2 py-1 text-white">Primary</span>
|
|
2534
|
+
<span class="rounded-md bg-export-subtle px-2 py-1 text-export">Subtle</span>
|
|
2535
|
+
<span class="text-export-light">Light accent text</span>
|
|
2536
|
+
<div class="rounded-xl bg-export-tint p-5">Tinted panel</div>
|
|
2537
|
+
`,to=`// STN / import orange \u2014 same idea as export; prefer ModeColorService when mode can flip.
|
|
2538
|
+
|
|
2539
|
+
<span class="rounded-md bg-import px-2 py-1 text-white">Primary</span>
|
|
2540
|
+
<span class="rounded-md bg-import-subtle px-2 py-1 text-import">Subtle</span>
|
|
2541
|
+
<span class="text-import-light">Light accent text</span>
|
|
2542
|
+
<div class="rounded-xl bg-import-tint p-5">Tinted panel</div>
|
|
2543
|
+
`,no=`// Status colors \u2014 mode-agnostic. Don't map danger/warning to export/import.
|
|
2544
|
+
|
|
2545
|
+
<span class="rounded-md bg-danger-subtle px-2 py-1 text-danger">Danger</span>
|
|
2546
|
+
<span class="rounded-md bg-danger px-2 py-1 text-white">Danger solid</span>
|
|
2547
|
+
<span class="rounded-md bg-warning-subtle px-2 py-1 text-warning-dark">Warning</span>
|
|
2548
|
+
<span class="text-danger-dark">Inline error copy</span>
|
|
2549
|
+
`,io=`// Typography scale from the theme preset. Prefer these over arbitrary text-[Npx].
|
|
2550
|
+
|
|
2551
|
+
<h1 class="text-heading-1 text-ink dark:text-white">Ship across continents</h1>
|
|
2552
|
+
<h2 class="text-heading-2 text-ink dark:text-white">Shipment details</h2>
|
|
2553
|
+
<h3 class="text-heading-3 text-ink dark:text-white">Cargo summary</h3>
|
|
2554
|
+
<p class="text-body-lg text-ink dark:text-white">
|
|
2555
|
+
Supporting sentence for denser marketing or empty states.
|
|
2556
|
+
</p>
|
|
2557
|
+
<p class="text-body text-ink dark:text-white">
|
|
2558
|
+
Default reading size for forms, tables, and dialogs.
|
|
2559
|
+
</p>
|
|
2560
|
+
<p class="text-body-sm text-neutral-600">Secondary copy and dense metadata.</p>
|
|
2561
|
+
<span class="text-caption text-neutral-600">Hints, timestamps, and chip labels.</span>
|
|
2562
|
+
`,ro=`// Primary chrome that follows SFN/STN. Bind [class] to modeColor.classes() literals \u2014
|
|
2563
|
+
// don't interpolate Tailwind token names yourself.
|
|
2564
|
+
|
|
2565
|
+
import { Component, inject } from '@angular/core';
|
|
2566
|
+
import { ModeColorService } from '@africanies/africanies-theme';
|
|
2567
|
+
import { ButtonComponent } from '@africanies/africanies-ui';
|
|
2568
|
+
|
|
2569
|
+
@Component({
|
|
2570
|
+
selector: 'app-mode-accent-demo',
|
|
2571
|
+
standalone: true,
|
|
2572
|
+
imports: [ButtonComponent],
|
|
2573
|
+
template: \`
|
|
2574
|
+
@let colors = modeColor.classes();
|
|
2575
|
+
|
|
2576
|
+
<button africanies-button type="button" [class]="colors.primary">
|
|
2577
|
+
Mode-aware primary CTA
|
|
2578
|
+
</button>
|
|
2579
|
+
|
|
2580
|
+
<span [class]="colors.text">Accent label</span>
|
|
2581
|
+
|
|
2582
|
+
<div class="rounded-xl p-5" [class]="colors.bgSubtle">
|
|
2583
|
+
<p class="m-0 text-caption font-medium uppercase tracking-wide" [class]="colors.text">
|
|
2584
|
+
SFN export / STN import
|
|
2585
|
+
</p>
|
|
2586
|
+
<p class="mt-2 m-0 text-heading-3 text-ink dark:text-white">Shipment headline</p>
|
|
2587
|
+
</div>
|
|
2588
|
+
|
|
2589
|
+
<div
|
|
2590
|
+
class="rounded-lg px-3 py-2 transition"
|
|
2591
|
+
[class]="colors.soft + ' ' + colors.softHover"
|
|
2592
|
+
>
|
|
2593
|
+
Selected row highlight
|
|
2594
|
+
</div>
|
|
2595
|
+
\`,
|
|
2596
|
+
})
|
|
2597
|
+
export class ModeAccentDemoComponent {
|
|
2598
|
+
protected readonly modeColor = inject(ModeColorService);
|
|
2599
|
+
}
|
|
2600
|
+
`,oo=`// Shared SVG sprite via <africanies-icon>. Type dynamic names as IconName for autocomplete.
|
|
2601
|
+
|
|
2602
|
+
import { Component } from '@angular/core';
|
|
2603
|
+
import { AfricaniesIconComponent, type IconName } from '@africanies/africanies-icons';
|
|
2604
|
+
|
|
2605
|
+
@Component({
|
|
2606
|
+
selector: 'app-icon-demo',
|
|
2607
|
+
standalone: true,
|
|
2608
|
+
imports: [AfricaniesIconComponent],
|
|
2609
|
+
template: \`
|
|
2610
|
+
<africanies-icon name="airplane" [size]="24" />
|
|
2611
|
+
<africanies-icon name="warehouse" [size]="24" />
|
|
2612
|
+
<africanies-icon name="warning" [size]="24" />
|
|
2613
|
+
<africanies-icon [name]="dynamicIcon" [size]="16" />
|
|
2614
|
+
\`,
|
|
2615
|
+
})
|
|
2616
|
+
export class IconDemoComponent {
|
|
2617
|
+
protected readonly dynamicIcon: IconName = 'anchor';
|
|
2618
|
+
}
|
|
2619
|
+
`;var so=`// Shared API/UI shapes \u2014 type-only, no providers. Import from here instead of reinventing.
|
|
2620
|
+
|
|
2621
|
+
import type {
|
|
2622
|
+
AsyncQueryStateModel,
|
|
2623
|
+
ApiResponseModel,
|
|
2624
|
+
CountryModel,
|
|
2625
|
+
CurrencyModel,
|
|
2626
|
+
PaginationMetaModel,
|
|
2627
|
+
PaginationQueryParamsModel,
|
|
2628
|
+
PaymentMethodModel,
|
|
2629
|
+
ResourceId,
|
|
2630
|
+
ShippingMode,
|
|
2631
|
+
ModeConfigDataModel,
|
|
2632
|
+
} from '@africanies/africanies-models';
|
|
2633
|
+
`;var co=`// You own the items array \u2014 breadcrumb won't walk the route tree for you.
|
|
2634
|
+
// Link every ancestor; leave routerLink off the last crumb (current page).
|
|
2635
|
+
|
|
2636
|
+
import { Component, computed, inject, signal } from '@angular/core';
|
|
2637
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
2638
|
+
import { NavigationEnd, Router } from '@angular/router';
|
|
2639
|
+
import { BreadcrumbComponent, type AfricaniesNavItem } from '@africanies/africanies-ui';
|
|
2640
|
+
import { filter, map, startWith } from 'rxjs';
|
|
2641
|
+
|
|
2642
|
+
@Component({
|
|
2643
|
+
selector: 'app-shipment-detail-chrome',
|
|
2644
|
+
standalone: true,
|
|
2645
|
+
imports: [BreadcrumbComponent],
|
|
2646
|
+
template: \`
|
|
2647
|
+
<africanies-breadcrumb [items]="crumbs()" />
|
|
2648
|
+
\`,
|
|
2649
|
+
})
|
|
2650
|
+
export class ShipmentDetailChromeComponent {
|
|
2651
|
+
private readonly router = inject(Router);
|
|
2652
|
+
|
|
2653
|
+
private readonly url = toSignal(
|
|
2654
|
+
this.router.events.pipe(
|
|
2655
|
+
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
|
|
2656
|
+
map(() => this.router.url),
|
|
2657
|
+
startWith(this.router.url),
|
|
2658
|
+
),
|
|
2659
|
+
{ initialValue: this.router.url },
|
|
2660
|
+
);
|
|
2661
|
+
|
|
2662
|
+
protected readonly shipmentRef = signal('SFN-1042');
|
|
2663
|
+
|
|
2664
|
+
protected readonly crumbs = computed((): AfricaniesNavItem[] => {
|
|
2665
|
+
const ref = this.shipmentRef();
|
|
2666
|
+
const path = this.url().split('?')[0] ?? '';
|
|
2667
|
+
const section = path.includes('/documents')
|
|
2668
|
+
? 'Documents'
|
|
2669
|
+
: path.includes('/events')
|
|
2670
|
+
? 'Events'
|
|
2671
|
+
: 'Overview';
|
|
2672
|
+
|
|
2673
|
+
return [
|
|
2674
|
+
{ id: 'home', label: 'Home', routerLink: '/overview', icon: 'home' },
|
|
2675
|
+
{ id: 'shipments', label: 'Shipments', routerLink: '/shipments' },
|
|
2676
|
+
{ id: 'detail', label: ref, routerLink: '/shipments/' + ref + '/overview' },
|
|
2677
|
+
{ id: 'section', label: section }, // current page \u2014 no routerLink
|
|
2678
|
+
];
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
`,po=`// URL-driven tabs + child routes. Active tab follows Router.isActive (survives refresh).
|
|
2682
|
+
// Put routerLink on each item, render <router-outlet /> below. Don't mix with africaniesTabDef.
|
|
2683
|
+
// keepScroll defaults to true \u2014 navigates with { scroll: 'manual' } so the page does not jump to the top.
|
|
2684
|
+
// Pass [keepScroll]="false" if you want the app's normal scrollPositionRestoration behaviour.
|
|
2685
|
+
|
|
2686
|
+
import { Component, signal } from '@angular/core';
|
|
2687
|
+
import { RouterOutlet } from '@angular/router';
|
|
2688
|
+
import { TabsComponent, type AfricaniesNavItem } from '@africanies/africanies-ui';
|
|
2689
|
+
|
|
2690
|
+
@Component({
|
|
2691
|
+
selector: 'app-shipment-detail-tabs',
|
|
2692
|
+
standalone: true,
|
|
2693
|
+
imports: [TabsComponent, RouterOutlet],
|
|
2694
|
+
template: \`
|
|
2695
|
+
<africanies-tabs
|
|
2696
|
+
[items]="routeTabs"
|
|
2697
|
+
[(activeId)]="routeTabId"
|
|
2698
|
+
ariaLabel="Shipment sections"
|
|
2699
|
+
/>
|
|
2700
|
+
|
|
2701
|
+
<div class="mt-4">
|
|
2702
|
+
<router-outlet />
|
|
2703
|
+
</div>
|
|
2704
|
+
\`,
|
|
2705
|
+
})
|
|
2706
|
+
export class ShipmentDetailTabsComponent {
|
|
2707
|
+
protected readonly routeTabId = signal<string | null>(null);
|
|
2708
|
+
|
|
2709
|
+
protected readonly routeTabs: AfricaniesNavItem[] = [
|
|
2710
|
+
{
|
|
2711
|
+
id: 'overview',
|
|
2712
|
+
label: 'Overview',
|
|
2713
|
+
icon: 'inbox',
|
|
2714
|
+
routerLink: '/shipments/SFN-1042/overview',
|
|
2715
|
+
},
|
|
2716
|
+
{
|
|
2717
|
+
id: 'documents',
|
|
2718
|
+
label: 'Documents',
|
|
2719
|
+
icon: 'file-add',
|
|
2720
|
+
routerLink: '/shipments/SFN-1042/documents',
|
|
2721
|
+
},
|
|
2722
|
+
{
|
|
2723
|
+
id: 'events',
|
|
2724
|
+
label: 'Events',
|
|
2725
|
+
icon: 'alarm',
|
|
2726
|
+
routerLink: '/shipments/SFN-1042/events',
|
|
2727
|
+
},
|
|
2728
|
+
];
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
// Example child route config (app.routes.ts excerpt):
|
|
2732
|
+
//
|
|
2733
|
+
// {
|
|
2734
|
+
// path: 'shipments/:ref',
|
|
2735
|
+
// component: ShipmentDetailTabsComponent,
|
|
2736
|
+
// children: [
|
|
2737
|
+
// { path: 'overview', loadComponent: () => import('./overview').then(m => m.OverviewPage) },
|
|
2738
|
+
// { path: 'documents', loadComponent: () => import('./documents').then(m => m.DocumentsPage) },
|
|
2739
|
+
// { path: 'events', loadComponent: () => import('./events').then(m => m.EventsPage) },
|
|
2740
|
+
// { path: '', pathMatch: 'full', redirectTo: 'overview' },
|
|
2741
|
+
// ],
|
|
2742
|
+
// }
|
|
2743
|
+
`,mo=`// Pill switcher on one path \u2014 same routerLink, different queryParams per segment.
|
|
2744
|
+
// Active pill matches the query on cold load.
|
|
2745
|
+
// keepScroll defaults to true \u2014 no jump to top when the query changes (same as routed tabs).
|
|
2746
|
+
|
|
2747
|
+
import { Component, inject, signal } from '@angular/core';
|
|
2748
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
2749
|
+
import { ActivatedRoute } from '@angular/router';
|
|
2750
|
+
import { SegmentComponent, type AfricaniesNavItem } from '@africanies/africanies-ui';
|
|
2751
|
+
import { map } from 'rxjs';
|
|
2752
|
+
|
|
2753
|
+
@Component({
|
|
2754
|
+
selector: 'app-shipment-list-density',
|
|
2755
|
+
standalone: true,
|
|
2756
|
+
imports: [SegmentComponent],
|
|
2757
|
+
template: \`
|
|
2758
|
+
<africanies-segment
|
|
2759
|
+
[items]="densitySegments"
|
|
2760
|
+
[(activeId)]="densityId"
|
|
2761
|
+
ariaLabel="List density"
|
|
2762
|
+
/>
|
|
2763
|
+
|
|
2764
|
+
<p class="mt-3 text-body-sm">
|
|
2765
|
+
Active density from URL:
|
|
2766
|
+
<span class="font-medium">{{ densityId() ?? '\u2014' }}</span>
|
|
2767
|
+
</p>
|
|
2768
|
+
\`,
|
|
2769
|
+
})
|
|
2770
|
+
export class ShipmentListDensityComponent {
|
|
2771
|
+
private readonly route = inject(ActivatedRoute);
|
|
2772
|
+
|
|
2773
|
+
protected readonly densityFromUrl = toSignal(
|
|
2774
|
+
this.route.queryParamMap.pipe(map((p) => p.get('density') ?? 'comfortable')),
|
|
2775
|
+
{ initialValue: 'comfortable' },
|
|
2776
|
+
);
|
|
2777
|
+
|
|
2778
|
+
protected readonly densityId = signal<string | null>(null);
|
|
2779
|
+
|
|
2780
|
+
protected readonly densitySegments: AfricaniesNavItem[] = [
|
|
2781
|
+
{
|
|
2782
|
+
id: 'comfortable',
|
|
2783
|
+
label: 'Comfortable',
|
|
2784
|
+
routerLink: '/shipments',
|
|
2785
|
+
queryParams: { density: 'comfortable' },
|
|
2786
|
+
},
|
|
2787
|
+
{
|
|
2788
|
+
id: 'compact',
|
|
2789
|
+
label: 'Compact',
|
|
2790
|
+
routerLink: '/shipments',
|
|
2791
|
+
queryParams: { density: 'compact' },
|
|
2792
|
+
},
|
|
2793
|
+
{
|
|
2794
|
+
id: 'dense',
|
|
2795
|
+
label: 'Dense',
|
|
2796
|
+
routerLink: '/shipments',
|
|
2797
|
+
queryParams: { density: 'dense' },
|
|
2798
|
+
},
|
|
2799
|
+
];
|
|
2800
|
+
}
|
|
2801
|
+
`,uo=`// In-page tabs \u2014 no router. Selection is [(activeId)] only; panels live in africaniesTabDef.
|
|
2802
|
+
// Match template ids to AfricaniesNavItem.id. Want bookmarkable tabs? Use routed tabs instead.
|
|
2803
|
+
|
|
2804
|
+
import { Component, signal } from '@angular/core';
|
|
2805
|
+
import { TabDefDirective, TabsComponent, type AfricaniesNavItem } from '@africanies/africanies-ui';
|
|
2806
|
+
|
|
2807
|
+
@Component({
|
|
2808
|
+
selector: 'app-shipment-editor-local-tabs',
|
|
2809
|
+
standalone: true,
|
|
2810
|
+
imports: [TabsComponent, TabDefDirective],
|
|
2811
|
+
template: \`
|
|
2812
|
+
<africanies-tabs
|
|
2813
|
+
[items]="localTabs"
|
|
2814
|
+
[(activeId)]="localTabId"
|
|
2815
|
+
ariaLabel="Editor sections"
|
|
2816
|
+
>
|
|
2817
|
+
<ng-template africaniesTabDef="general">
|
|
2818
|
+
<p class="m-0 text-body">General fields \u2014 not tied to the URL.</p>
|
|
2819
|
+
</ng-template>
|
|
2820
|
+
|
|
2821
|
+
<ng-template africaniesTabDef="parties">
|
|
2822
|
+
<p class="m-0 text-body">Shipper and consignee \u2014 local panel only.</p>
|
|
2823
|
+
</ng-template>
|
|
2824
|
+
|
|
2825
|
+
<ng-template africaniesTabDef="charges">
|
|
2826
|
+
<p class="m-0 text-body">Charges and duties \u2014 local panel only.</p>
|
|
2827
|
+
</ng-template>
|
|
2828
|
+
</africanies-tabs>
|
|
2829
|
+
\`,
|
|
2830
|
+
})
|
|
2831
|
+
export class ShipmentEditorLocalTabsComponent {
|
|
2832
|
+
protected readonly localTabId = signal('general');
|
|
2833
|
+
|
|
2834
|
+
protected readonly localTabs: AfricaniesNavItem[] = [
|
|
2835
|
+
{ id: 'general', label: 'General', icon: 'edit' },
|
|
2836
|
+
{ id: 'parties', label: 'Parties', icon: 'user' },
|
|
2837
|
+
{ id: 'charges', label: 'Charges', icon: 'wallet' },
|
|
2838
|
+
];
|
|
2839
|
+
}
|
|
2840
|
+
`,fo=`// Ink-spine side nav \u2014 icons, nested children, collapse to icon rail.
|
|
2841
|
+
// Soft active highlight; collapsed: hover an icon for a label blade.
|
|
2842
|
+
// Collapse all sits above the list (top right) when any section is open.
|
|
2843
|
+
|
|
2844
|
+
import { Component, signal } from '@angular/core';
|
|
2845
|
+
import {
|
|
2846
|
+
SideNavComponent,
|
|
2847
|
+
type AfricaniesSideNavItem,
|
|
2848
|
+
} from '@africanies/africanies-ui';
|
|
2849
|
+
|
|
2850
|
+
@Component({
|
|
2851
|
+
selector: 'app-shell-nav',
|
|
2852
|
+
standalone: true,
|
|
2853
|
+
imports: [SideNavComponent],
|
|
2854
|
+
template: \`
|
|
2855
|
+
<div class="h-[28rem] overflow-visible rounded-xl border border-border dark:border-white/10">
|
|
2856
|
+
<africanies-side-nav
|
|
2857
|
+
[items]="nav"
|
|
2858
|
+
[(collapsed)]="collapsed"
|
|
2859
|
+
[(activeId)]="activeId"
|
|
2860
|
+
ariaLabel="Portal"
|
|
2861
|
+
>
|
|
2862
|
+
</africanies-side-nav>
|
|
2863
|
+
</div>
|
|
2864
|
+
\`,
|
|
2865
|
+
})
|
|
2866
|
+
export class ShellNavComponent {
|
|
2867
|
+
protected readonly collapsed = signal(false);
|
|
2868
|
+
protected readonly activeId = signal('shipments');
|
|
2869
|
+
|
|
2870
|
+
protected readonly nav: AfricaniesSideNavItem[] = [
|
|
2871
|
+
{ id: 'home', label: 'Home', icon: 'home' },
|
|
2872
|
+
{
|
|
2873
|
+
id: 'shipments',
|
|
2874
|
+
label: 'Shipments',
|
|
2875
|
+
icon: 'truck',
|
|
2876
|
+
badge: true,
|
|
2877
|
+
children: [
|
|
2878
|
+
{ id: 'track', label: 'Track', icon: 'map-marker', badge: true },
|
|
2879
|
+
{ id: 'create', label: 'Create', icon: 'plus' },
|
|
2880
|
+
],
|
|
2881
|
+
},
|
|
2882
|
+
{ id: 'warehouse', label: 'Warehouse', icon: 'warehouse' },
|
|
2883
|
+
{ id: 'settings', label: 'Settings', icon: 'cog' },
|
|
2884
|
+
];
|
|
2885
|
+
}
|
|
2886
|
+
`,go=`// Dedicated app shell \u2014 breadcrumbs + Back in the content column;
|
|
2887
|
+
// header holds clock, notifications, and avatar.
|
|
2888
|
+
// Needs provideAfricaniesUiOverlays() for the notification drawer.
|
|
2889
|
+
// Back is built into africanies-app-shell \u2014 nest child routes under a parent and it appears.
|
|
2890
|
+
// Log out: confirm \u2192 UserService.logoutFromAllSessions() \u2192 AuthTokenService.clear().
|
|
2891
|
+
|
|
2892
|
+
import { Component, inject, signal } from '@angular/core';
|
|
2893
|
+
|
|
2894
|
+
import { finalize } from 'rxjs';
|
|
2895
|
+
|
|
2896
|
+
import { AuthTokenService, UserService } from '@africanies/africanies-core';
|
|
2897
|
+
import {
|
|
2898
|
+
AppShellComponent,
|
|
2899
|
+
ConfirmService,
|
|
2900
|
+
SideNavComponent,
|
|
2901
|
+
type AfricaniesMenuItem,
|
|
2902
|
+
type AfricaniesNavItem,
|
|
2903
|
+
type AfricaniesNotification,
|
|
2904
|
+
type AfricaniesSideNavItem,
|
|
2905
|
+
} from '@africanies/africanies-ui';
|
|
2906
|
+
|
|
2907
|
+
@Component({
|
|
2908
|
+
selector: 'app-product-shell',
|
|
2909
|
+
standalone: true,
|
|
2910
|
+
imports: [AppShellComponent, SideNavComponent],
|
|
2911
|
+
template: \`
|
|
2912
|
+
<africanies-app-shell
|
|
2913
|
+
contentWidth="5xl"
|
|
2914
|
+
[headerTitle]="pageTitle"
|
|
2915
|
+
[breadcrumbs]="crumbs"
|
|
2916
|
+
[catalogNav]="nav"
|
|
2917
|
+
userName="Jane Doe"
|
|
2918
|
+
[userMenuItems]="accountMenu"
|
|
2919
|
+
[notifications]="notifications"
|
|
2920
|
+
>
|
|
2921
|
+
<africanies-side-nav
|
|
2922
|
+
sidenav
|
|
2923
|
+
[items]="nav"
|
|
2924
|
+
[(collapsed)]="collapsed"
|
|
2925
|
+
[(activeId)]="activeId"
|
|
2926
|
+
>
|
|
2927
|
+
</africanies-side-nav>
|
|
2928
|
+
|
|
2929
|
+
<router-outlet />
|
|
2930
|
+
</africanies-app-shell>
|
|
2931
|
+
\`,
|
|
2932
|
+
})
|
|
2933
|
+
export class ProductShellComponent {
|
|
2934
|
+
private readonly confirm = inject(ConfirmService);
|
|
2935
|
+
private readonly users = inject(UserService);
|
|
2936
|
+
private readonly auth = inject(AuthTokenService);
|
|
2937
|
+
|
|
2938
|
+
protected readonly collapsed = signal(false);
|
|
2939
|
+
protected readonly activeId = signal('home');
|
|
2940
|
+
protected readonly pageTitle = 'STN-1042';
|
|
2941
|
+
|
|
2942
|
+
protected readonly crumbs: AfricaniesNavItem[] = [
|
|
2943
|
+
{ id: 'home', label: 'Home', icon: 'home' },
|
|
2944
|
+
{ id: 'shipments', label: 'Shipments' },
|
|
2945
|
+
{ id: 'track', label: 'STN-1042' },
|
|
2946
|
+
];
|
|
2947
|
+
|
|
2948
|
+
protected readonly accountMenu: AfricaniesMenuItem[] = [
|
|
2949
|
+
{ label: 'Profile', icon: 'user', onClick: () => {} },
|
|
2950
|
+
{ label: 'Settings', icon: 'cog', onClick: () => {} },
|
|
2951
|
+
{
|
|
2952
|
+
label: 'Log out',
|
|
2953
|
+
icon: 'sign-out',
|
|
2954
|
+
danger: true,
|
|
2955
|
+
dividerBefore: true,
|
|
2956
|
+
onClick: () => this.confirmLogout(),
|
|
2957
|
+
},
|
|
2958
|
+
];
|
|
2959
|
+
|
|
2960
|
+
protected readonly notifications: AfricaniesNotification[] = [
|
|
2961
|
+
{
|
|
2962
|
+
id: 'n1',
|
|
2963
|
+
title: 'Shipment delivered',
|
|
2964
|
+
timestamp: new Date().toISOString(),
|
|
2965
|
+
},
|
|
2966
|
+
];
|
|
2967
|
+
|
|
2968
|
+
protected readonly nav: AfricaniesSideNavItem[] = [
|
|
2969
|
+
{ id: 'home', label: 'Home', icon: 'home' },
|
|
2970
|
+
{ id: 'shipments', label: 'Shipments', icon: 'truck' },
|
|
2971
|
+
];
|
|
2972
|
+
|
|
2973
|
+
protected confirmLogout(): void {
|
|
2974
|
+
this.confirm
|
|
2975
|
+
.confirm({
|
|
2976
|
+
title: 'Log out?',
|
|
2977
|
+
message:
|
|
2978
|
+
'This signs you out of this device and every other session.',
|
|
2979
|
+
confirmLabel: 'Log out',
|
|
2980
|
+
danger: true,
|
|
2981
|
+
})
|
|
2982
|
+
.subscribe((ok) => {
|
|
2983
|
+
if (!ok) {
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
this.users
|
|
2987
|
+
.logoutFromAllSessions()
|
|
2988
|
+
.pipe(finalize(() => this.auth.clear()))
|
|
2989
|
+
.subscribe();
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
`;var bo=`// Centered modal via ModalService. Needs provideAfricaniesUiOverlays() in app.config.
|
|
2994
|
+
// Built on Angular CDK Overlay (panels attach to body \u2014 not clipped by overflow).
|
|
2995
|
+
// Inject OVERLAY_DATA in the panel, close with AfricaniesOverlayRef.close(result).
|
|
2996
|
+
// dismissible defaults to false \u2014 backdrop/Escape only work when you opt in.
|
|
2997
|
+
// Close X: put it in a shrink-0 header and scroll the body (host overflow-hidden
|
|
2998
|
+
// + africanies-overlay-scroll). The pane reserves a scrollbar gutter so the track
|
|
2999
|
+
// cannot cover the button.
|
|
3000
|
+
|
|
3001
|
+
// app.config.ts
|
|
3002
|
+
import { ApplicationConfig } from '@angular/core';
|
|
3003
|
+
import { provideAfricaniesUiOverlays } from '@africanies/africanies-ui';
|
|
3004
|
+
|
|
3005
|
+
export const appConfig: ApplicationConfig = {
|
|
3006
|
+
providers: [
|
|
3007
|
+
provideAfricaniesUiOverlays(),
|
|
3008
|
+
],
|
|
3009
|
+
};
|
|
3010
|
+
|
|
3011
|
+
// --- Hosted panel ---
|
|
3012
|
+
import { Component, inject } from '@angular/core';
|
|
3013
|
+
import {
|
|
3014
|
+
AfricaniesOverlayRef,
|
|
3015
|
+
ButtonComponent,
|
|
3016
|
+
OVERLAY_DATA,
|
|
3017
|
+
} from '@africanies/africanies-ui';
|
|
3018
|
+
|
|
3019
|
+
export interface EditShipmentData {
|
|
3020
|
+
shipmentRef: string;
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
export interface EditShipmentResult {
|
|
3024
|
+
saved: boolean;
|
|
3025
|
+
note: string;
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
@Component({
|
|
3029
|
+
selector: 'app-edit-shipment-modal',
|
|
3030
|
+
standalone: true,
|
|
3031
|
+
imports: [ButtonComponent],
|
|
3032
|
+
template: \`
|
|
3033
|
+
<div class="flex flex-col gap-5">
|
|
3034
|
+
<h2 class="m-0 text-heading-3 text-ink dark:text-white">
|
|
3035
|
+
Edit {{ data.shipmentRef }}
|
|
3036
|
+
</h2>
|
|
3037
|
+
|
|
3038
|
+
<label class="flex flex-col gap-1.5">
|
|
3039
|
+
<span class="text-caption font-medium text-neutral-600">Note</span>
|
|
3040
|
+
<input
|
|
3041
|
+
class="rounded-md border border-border px-3 py-2 text-body"
|
|
3042
|
+
type="text"
|
|
3043
|
+
[value]="note"
|
|
3044
|
+
(input)="note = $any($event.target).value"
|
|
3045
|
+
/>
|
|
3046
|
+
</label>
|
|
3047
|
+
|
|
3048
|
+
<div class="flex justify-end gap-2">
|
|
3049
|
+
<button africanies-button type="button" variant="ghost" (click)="ref.close()">
|
|
3050
|
+
Cancel
|
|
3051
|
+
</button>
|
|
3052
|
+
<button
|
|
3053
|
+
africanies-button
|
|
3054
|
+
type="button"
|
|
3055
|
+
variant="primary"
|
|
3056
|
+
(click)="ref.close({ saved: true, note })"
|
|
3057
|
+
>
|
|
3058
|
+
Save
|
|
3059
|
+
</button>
|
|
3060
|
+
</div>
|
|
3061
|
+
</div>
|
|
3062
|
+
\`,
|
|
3063
|
+
})
|
|
3064
|
+
export class EditShipmentModalComponent {
|
|
3065
|
+
protected readonly data = inject<EditShipmentData>(OVERLAY_DATA);
|
|
3066
|
+
protected readonly ref = inject(AfricaniesOverlayRef<EditShipmentResult>);
|
|
3067
|
+
protected note = '';
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
// --- Caller ---
|
|
3071
|
+
import { Component, inject } from '@angular/core';
|
|
3072
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
3073
|
+
import { ButtonComponent, ModalService } from '@africanies/africanies-ui';
|
|
3074
|
+
|
|
3075
|
+
@Component({
|
|
3076
|
+
selector: 'app-open-modal-demo',
|
|
3077
|
+
standalone: true,
|
|
3078
|
+
imports: [ButtonComponent],
|
|
3079
|
+
template: \`
|
|
3080
|
+
<button africanies-button type="button" variant="primary" (click)="openLocked()">
|
|
3081
|
+
Edit shipment
|
|
3082
|
+
</button>
|
|
3083
|
+
<button africanies-button type="button" variant="secondary" (click)="openDismissible()">
|
|
3084
|
+
Quick look
|
|
3085
|
+
</button>
|
|
3086
|
+
\`,
|
|
3087
|
+
})
|
|
3088
|
+
export class OpenModalDemoComponent {
|
|
3089
|
+
private readonly modal = inject(ModalService);
|
|
3090
|
+
|
|
3091
|
+
protected openLocked(): void {
|
|
3092
|
+
this.modal
|
|
3093
|
+
.open<EditShipmentData, EditShipmentResult>(EditShipmentModalComponent, {
|
|
3094
|
+
data: { shipmentRef: 'SFN-1042' },
|
|
3095
|
+
// dismissible defaults to false \u2014 backdrop / Escape do nothing.
|
|
3096
|
+
// size defaults to 'md'. Use 'lg' for dense forms, 'xl' for media.
|
|
3097
|
+
})
|
|
3098
|
+
.afterClosed()
|
|
3099
|
+
.pipe(takeUntilDestroyed())
|
|
3100
|
+
.subscribe((result) => {
|
|
3101
|
+
if (result?.saved) {
|
|
3102
|
+
// Apply note locally or invalidate list query \u2014 no second GET needed.
|
|
3103
|
+
}
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
protected openDismissible(): void {
|
|
3108
|
+
this.modal
|
|
3109
|
+
.open<EditShipmentData, EditShipmentResult>(EditShipmentModalComponent, {
|
|
3110
|
+
data: { shipmentRef: 'SFN-1042' },
|
|
3111
|
+
dismissible: true,
|
|
3112
|
+
size: 'lg',
|
|
3113
|
+
})
|
|
3114
|
+
.afterClosed()
|
|
3115
|
+
.pipe(takeUntilDestroyed())
|
|
3116
|
+
.subscribe((result) => {
|
|
3117
|
+
// result undefined when dismissed via backdrop / Escape / Cancel without save.
|
|
3118
|
+
});
|
|
3119
|
+
}
|
|
3120
|
+
}`,yo=`// Right-edge drawer \u2014 same OVERLAY_DATA / AfricaniesOverlayRef contract as modals.
|
|
3121
|
+
// Good for filters and detail chrome. Default is locked; set dismissible: true for abandonable panels.
|
|
3122
|
+
|
|
3123
|
+
import { Component, inject } from '@angular/core';
|
|
3124
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
3125
|
+
import {
|
|
3126
|
+
AfricaniesOverlayRef,
|
|
3127
|
+
ButtonComponent,
|
|
3128
|
+
DrawerService,
|
|
3129
|
+
OVERLAY_DATA,
|
|
3130
|
+
} from '@africanies/africanies-ui';
|
|
3131
|
+
|
|
3132
|
+
export interface FiltersDrawerData {
|
|
3133
|
+
facet: string;
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
export interface FiltersDrawerResult {
|
|
3137
|
+
applied: boolean;
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
@Component({
|
|
3141
|
+
selector: 'app-filters-drawer',
|
|
3142
|
+
standalone: true,
|
|
3143
|
+
imports: [ButtonComponent],
|
|
3144
|
+
template: \`
|
|
3145
|
+
<div class="flex h-full flex-col gap-5">
|
|
3146
|
+
<h2 class="m-0 text-heading-3 text-ink dark:text-white">
|
|
3147
|
+
Filters \u2014 {{ data.facet }}
|
|
3148
|
+
</h2>
|
|
3149
|
+
|
|
3150
|
+
<ul class="m-0 flex list-none flex-col gap-2 p-0">
|
|
3151
|
+
@for (opt of options; track opt) {
|
|
3152
|
+
<li>
|
|
3153
|
+
<label class="flex items-center gap-2 rounded-md border border-border px-3 py-2">
|
|
3154
|
+
<input type="checkbox" />
|
|
3155
|
+
{{ opt }}
|
|
3156
|
+
</label>
|
|
3157
|
+
</li>
|
|
3158
|
+
}
|
|
3159
|
+
</ul>
|
|
3160
|
+
|
|
3161
|
+
<div class="mt-auto flex justify-end gap-2 pt-4">
|
|
3162
|
+
<button africanies-button type="button" variant="ghost" (click)="ref.close()">
|
|
3163
|
+
Cancel
|
|
3164
|
+
</button>
|
|
3165
|
+
<button
|
|
3166
|
+
africanies-button
|
|
3167
|
+
type="button"
|
|
3168
|
+
variant="primary"
|
|
3169
|
+
(click)="ref.close({ applied: true })"
|
|
3170
|
+
>
|
|
3171
|
+
Apply
|
|
3172
|
+
</button>
|
|
3173
|
+
</div>
|
|
3174
|
+
</div>
|
|
3175
|
+
\`,
|
|
3176
|
+
})
|
|
3177
|
+
export class FiltersDrawerComponent {
|
|
3178
|
+
protected readonly data = inject<FiltersDrawerData>(OVERLAY_DATA);
|
|
3179
|
+
protected readonly ref = inject(AfricaniesOverlayRef<FiltersDrawerResult>);
|
|
3180
|
+
protected readonly options = ['In transit', 'Delivered', 'Pending', 'Exception'] as const;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
@Component({
|
|
3184
|
+
selector: 'app-open-drawer-demo',
|
|
3185
|
+
standalone: true,
|
|
3186
|
+
imports: [ButtonComponent],
|
|
3187
|
+
template: \`
|
|
3188
|
+
<button africanies-button type="button" variant="secondary" (click)="open(false)">
|
|
3189
|
+
Open (locked)
|
|
3190
|
+
</button>
|
|
3191
|
+
<button africanies-button type="button" variant="primary" (click)="open(true)">
|
|
3192
|
+
Open (dismissible)
|
|
3193
|
+
</button>
|
|
3194
|
+
\`,
|
|
3195
|
+
})
|
|
3196
|
+
export class OpenDrawerDemoComponent {
|
|
3197
|
+
private readonly drawer = inject(DrawerService);
|
|
3198
|
+
|
|
3199
|
+
protected open(dismissible: boolean): void {
|
|
3200
|
+
this.drawer
|
|
3201
|
+
.open<FiltersDrawerData, FiltersDrawerResult>(FiltersDrawerComponent, {
|
|
3202
|
+
data: { facet: 'Status' },
|
|
3203
|
+
dismissible,
|
|
3204
|
+
})
|
|
3205
|
+
.afterClosed()
|
|
3206
|
+
.pipe(takeUntilDestroyed())
|
|
3207
|
+
.subscribe((result) => {
|
|
3208
|
+
if (result?.applied) {
|
|
3209
|
+
// Push facet selections into list query params \u2192 refetch.
|
|
3210
|
+
}
|
|
3211
|
+
});
|
|
3212
|
+
}
|
|
3213
|
+
}`,vo=`// One-liner confirm via ConfirmService \u2014 returns Observable<boolean>.
|
|
3214
|
+
// Set danger: true for delete/void. Don't open ConfirmDialogComponent yourself.
|
|
3215
|
+
|
|
3216
|
+
import { Component, inject } from '@angular/core';
|
|
3217
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
3218
|
+
import { ButtonComponent, ConfirmService } from '@africanies/africanies-ui';
|
|
3219
|
+
|
|
3220
|
+
@Component({
|
|
3221
|
+
selector: 'app-confirm-demo',
|
|
3222
|
+
standalone: true,
|
|
3223
|
+
imports: [ButtonComponent],
|
|
3224
|
+
template: \`
|
|
3225
|
+
<button africanies-button type="button" variant="secondary" (click)="confirmNeutral()">
|
|
3226
|
+
Mark delivered
|
|
3227
|
+
</button>
|
|
3228
|
+
|
|
3229
|
+
<button africanies-button type="button" variant="danger" (click)="confirmDestructive()">
|
|
3230
|
+
Delete shipment
|
|
3231
|
+
</button>
|
|
3232
|
+
|
|
3233
|
+
<button africanies-button type="button" variant="ghost" (click)="confirmDismissible()">
|
|
3234
|
+
Dismissible confirm
|
|
3235
|
+
</button>
|
|
3236
|
+
\`,
|
|
3237
|
+
})
|
|
3238
|
+
export class ConfirmDemoComponent {
|
|
3239
|
+
private readonly confirm = inject(ConfirmService);
|
|
3240
|
+
|
|
3241
|
+
protected confirmNeutral(): void {
|
|
3242
|
+
this.confirm
|
|
3243
|
+
.confirm({
|
|
3244
|
+
title: 'Mark as delivered?',
|
|
3245
|
+
message: 'The consignee will be notified that the shipment is complete.',
|
|
3246
|
+
confirmLabel: 'Mark delivered',
|
|
3247
|
+
cancelLabel: 'Cancel',
|
|
3248
|
+
danger: false,
|
|
3249
|
+
})
|
|
3250
|
+
.pipe(takeUntilDestroyed())
|
|
3251
|
+
.subscribe((ok) => {
|
|
3252
|
+
if (ok) {
|
|
3253
|
+
this.markDelivered();
|
|
3254
|
+
}
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
protected confirmDestructive(): void {
|
|
3259
|
+
this.confirm
|
|
3260
|
+
.confirm({
|
|
3261
|
+
title: 'Delete shipment?',
|
|
3262
|
+
message: 'This cannot be undone. Related documents stay in archive.',
|
|
3263
|
+
confirmLabel: 'Delete',
|
|
3264
|
+
cancelLabel: 'Cancel',
|
|
3265
|
+
danger: true,
|
|
3266
|
+
// Optional: keep the dialog open with a loading Confirm until this
|
|
3267
|
+
// settles. Errors leave it open so the user can retry or cancel.
|
|
3268
|
+
// onConfirm: () => this.shipments.delete(id),
|
|
3269
|
+
})
|
|
3270
|
+
.pipe(takeUntilDestroyed())
|
|
3271
|
+
.subscribe((ok) => {
|
|
3272
|
+
if (ok) {
|
|
3273
|
+
this.deleteShipment();
|
|
3274
|
+
}
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
protected confirmDismissible(): void {
|
|
3279
|
+
this.confirm
|
|
3280
|
+
.confirm({
|
|
3281
|
+
title: 'Discard unsaved changes?',
|
|
3282
|
+
message: 'Your draft note will be lost.',
|
|
3283
|
+
confirmLabel: 'Discard',
|
|
3284
|
+
cancelLabel: 'Keep editing',
|
|
3285
|
+
danger: true,
|
|
3286
|
+
dismissible: true,
|
|
3287
|
+
// backdrop / Escape \u2192 false (same as Cancel) via ConfirmService map.
|
|
3288
|
+
})
|
|
3289
|
+
.pipe(takeUntilDestroyed())
|
|
3290
|
+
.subscribe((ok) => {
|
|
3291
|
+
if (ok) {
|
|
3292
|
+
this.discardDraft();
|
|
3293
|
+
}
|
|
3294
|
+
});
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
private markDelivered(): void {
|
|
3298
|
+
// mutation.mutate({ id, status: 'Delivered' })
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
private deleteShipment(): void {
|
|
3302
|
+
// mutation.mutate({ id })
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
private discardDraft(): void {
|
|
3306
|
+
// navigate away or ref.close() on parent overlay
|
|
3307
|
+
}
|
|
3308
|
+
}`;var Co=`// Multi-step wizard with forward-blocking. Match africaniesStepDef keys to StepDefinition.key.
|
|
3309
|
+
// Drive isValid from real form state; wire your own Back/Next around canGoNext().
|
|
3310
|
+
// This is wizard nav \u2014 not a shipment status timeline.
|
|
3311
|
+
|
|
3312
|
+
import { Component, computed, signal } from '@angular/core';
|
|
3313
|
+
import {
|
|
3314
|
+
ButtonComponent,
|
|
3315
|
+
StepDefDirective,
|
|
3316
|
+
StepperComponent,
|
|
3317
|
+
TextInputComponent,
|
|
3318
|
+
type StepDefinition,
|
|
3319
|
+
} from '@africanies/africanies-ui';
|
|
3320
|
+
|
|
3321
|
+
@Component({
|
|
3322
|
+
selector: 'app-shipment-wizard',
|
|
3323
|
+
standalone: true,
|
|
3324
|
+
imports: [
|
|
3325
|
+
StepperComponent,
|
|
3326
|
+
StepDefDirective,
|
|
3327
|
+
ButtonComponent,
|
|
3328
|
+
TextInputComponent,
|
|
3329
|
+
],
|
|
3330
|
+
template: \`
|
|
3331
|
+
<africanies-stepper
|
|
3332
|
+
[steps]="steps()"
|
|
3333
|
+
[activeIndex]="activeIndex()"
|
|
3334
|
+
[linear]="true"
|
|
3335
|
+
(activeIndexChange)="activeIndex.set($event)"
|
|
3336
|
+
>
|
|
3337
|
+
<ng-template africaniesStepDef="route">
|
|
3338
|
+
<div class="flex flex-col gap-4">
|
|
3339
|
+
<p class="m-0 text-body-sm text-neutral-600">
|
|
3340
|
+
Origin and destination for this shipment.
|
|
3341
|
+
</p>
|
|
3342
|
+
<div class="grid gap-4 md:grid-cols-2">
|
|
3343
|
+
<africanies-text-input label="Origin city" [(value)]="origin" />
|
|
3344
|
+
<africanies-text-input label="Destination city" [(value)]="destination" />
|
|
3345
|
+
</div>
|
|
3346
|
+
</div>
|
|
3347
|
+
</ng-template>
|
|
3348
|
+
|
|
3349
|
+
<ng-template africaniesStepDef="cargo">
|
|
3350
|
+
<div class="flex flex-col gap-4">
|
|
3351
|
+
<p class="m-0 text-body-sm text-neutral-600">Describe what you are shipping.</p>
|
|
3352
|
+
<africanies-text-input label="Commodity" [(value)]="commodity" />
|
|
3353
|
+
</div>
|
|
3354
|
+
</ng-template>
|
|
3355
|
+
|
|
3356
|
+
<ng-template africaniesStepDef="review">
|
|
3357
|
+
<div
|
|
3358
|
+
class="rounded-lg border border-border bg-background-welcome p-4 text-body-sm dark:border-white/10 dark:bg-ink-950"
|
|
3359
|
+
>
|
|
3360
|
+
<p class="m-0 font-medium text-ink dark:text-white">Review</p>
|
|
3361
|
+
<ul class="mt-2 mb-0 list-disc pl-5 text-neutral-600">
|
|
3362
|
+
<li>{{ origin() || '\u2014' }} \u2192 {{ destination() || '\u2014' }}</li>
|
|
3363
|
+
<li>{{ commodity() || 'No commodity set' }}</li>
|
|
3364
|
+
</ul>
|
|
3365
|
+
</div>
|
|
3366
|
+
</ng-template>
|
|
3367
|
+
</africanies-stepper>
|
|
3368
|
+
|
|
3369
|
+
<div class="mt-5 flex gap-2">
|
|
3370
|
+
<button
|
|
3371
|
+
africanies-button
|
|
3372
|
+
type="button"
|
|
3373
|
+
variant="ghost"
|
|
3374
|
+
[disabled]="activeIndex() === 0"
|
|
3375
|
+
(click)="activeIndex.set(activeIndex() - 1)"
|
|
3376
|
+
>
|
|
3377
|
+
Back
|
|
3378
|
+
</button>
|
|
3379
|
+
<button
|
|
3380
|
+
africanies-button
|
|
3381
|
+
type="button"
|
|
3382
|
+
[disabled]="activeIndex() >= steps().length - 1 || !canGoNext()"
|
|
3383
|
+
(click)="activeIndex.set(activeIndex() + 1)"
|
|
3384
|
+
>
|
|
3385
|
+
Next
|
|
3386
|
+
</button>
|
|
3387
|
+
</div>
|
|
3388
|
+
\`,
|
|
3389
|
+
})
|
|
3390
|
+
export class ShipmentWizardComponent {
|
|
3391
|
+
protected readonly activeIndex = signal(0);
|
|
3392
|
+
protected readonly routeValid = signal(false);
|
|
3393
|
+
|
|
3394
|
+
protected readonly origin = signal('Lagos');
|
|
3395
|
+
protected readonly destination = signal('');
|
|
3396
|
+
protected readonly commodity = signal('');
|
|
3397
|
+
|
|
3398
|
+
protected readonly steps = computed<StepDefinition[]>(() => [
|
|
3399
|
+
{ key: 'route', label: 'Route', isValid: this.routeValid() },
|
|
3400
|
+
{ key: 'cargo', label: 'Cargo', isValid: true },
|
|
3401
|
+
{ key: 'review', label: 'Review' },
|
|
3402
|
+
]);
|
|
3403
|
+
|
|
3404
|
+
protected canGoNext(): boolean {
|
|
3405
|
+
const current = this.steps()[this.activeIndex()];
|
|
3406
|
+
return current?.isValid !== false;
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
`,xo=`// Non-linear \u2014 users can jump to any step via the header.
|
|
3410
|
+
// isValid won't gate navigation here; use linear=true when order matters.
|
|
3411
|
+
|
|
3412
|
+
import { Component, signal } from '@angular/core';
|
|
3413
|
+
import {
|
|
3414
|
+
StepDefDirective,
|
|
3415
|
+
StepperComponent,
|
|
3416
|
+
type StepDefinition,
|
|
3417
|
+
} from '@africanies/africanies-ui';
|
|
3418
|
+
|
|
3419
|
+
@Component({
|
|
3420
|
+
selector: 'app-account-setup',
|
|
3421
|
+
standalone: true,
|
|
3422
|
+
imports: [StepperComponent, StepDefDirective],
|
|
3423
|
+
template: \`
|
|
3424
|
+
<africanies-stepper
|
|
3425
|
+
[steps]="steps"
|
|
3426
|
+
[activeIndex]="activeIndex()"
|
|
3427
|
+
[linear]="false"
|
|
3428
|
+
(activeIndexChange)="activeIndex.set($event)"
|
|
3429
|
+
>
|
|
3430
|
+
<ng-template africaniesStepDef="account">
|
|
3431
|
+
<p class="m-0 text-body text-ink dark:text-white">Account details step body.</p>
|
|
3432
|
+
</ng-template>
|
|
3433
|
+
|
|
3434
|
+
<ng-template africaniesStepDef="billing">
|
|
3435
|
+
<p class="m-0 text-body text-ink dark:text-white">Billing step body.</p>
|
|
3436
|
+
</ng-template>
|
|
3437
|
+
|
|
3438
|
+
<ng-template africaniesStepDef="confirm">
|
|
3439
|
+
<p class="m-0 text-body text-ink dark:text-white">Confirmation step body.</p>
|
|
3440
|
+
</ng-template>
|
|
3441
|
+
</africanies-stepper>
|
|
3442
|
+
\`,
|
|
3443
|
+
})
|
|
3444
|
+
export class AccountSetupComponent {
|
|
3445
|
+
protected readonly activeIndex = signal(0);
|
|
3446
|
+
|
|
3447
|
+
protected readonly steps: StepDefinition[] = [
|
|
3448
|
+
{ key: 'account', label: 'Account' },
|
|
3449
|
+
{ key: 'billing', label: 'Billing' },
|
|
3450
|
+
{ key: 'confirm', label: 'Confirm' },
|
|
3451
|
+
];
|
|
3452
|
+
}
|
|
3453
|
+
`;var Eo=`// Server-driven list: keep the table mounted; loading / empty / error render in the body.
|
|
3454
|
+
// Toolbar: Refresh (left); Filters + Export (right). Pass [meta] for the built-in pager.
|
|
3455
|
+
// Hydrate page/size/filters from FilterQueryService when the URL has those queries.
|
|
3456
|
+
// Pager + Apply write the same keys back. Refetch on sortChange / pageChange / sizeChange.
|
|
3457
|
+
// Refresh / tab focus \u2192 [refreshing] (rows stay). Page/size \u2192 pager spinner.
|
|
3458
|
+
// First load and STN\u2194SFN (listFetchKind reason 'mode') \u2192 clear rows + body loader.
|
|
3459
|
+
// Empty and hard errors stay in-grid so Filters / Export / pager remain available.
|
|
3460
|
+
// Expandable rows: africaniesRowDetail="Label" let-row for label / component value pairs.
|
|
3461
|
+
// Column width: omit to size to content; set width (e.g. '3.5rem') to pin actions.
|
|
3462
|
+
// The actions column sticks to the right while the table scrolls horizontally.
|
|
3463
|
+
|
|
3464
|
+
import { Component, computed, effect, inject, signal, untracked } from '@angular/core';
|
|
3465
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
3466
|
+
import { ApiClient, downloadCsv, listFetchKind, ShippingModeService, type ListFetchReason } from '@africanies/africanies-core';
|
|
3467
|
+
import type { AsyncQueryStateModel, PaginationMetaModel } from '@africanies/africanies-models';
|
|
3468
|
+
import {
|
|
3469
|
+
ActionMenuComponent,
|
|
3470
|
+
CellDefDirective,
|
|
3471
|
+
ChipComponent,
|
|
3472
|
+
CopyButtonComponent,
|
|
3473
|
+
DEFAULT_PAGE_SIZE,
|
|
3474
|
+
FilterQueryService,
|
|
3475
|
+
RowDetailDefDirective,
|
|
3476
|
+
TableComponent,
|
|
3477
|
+
trackShipmentsFilterConfig,
|
|
3478
|
+
type AfricaniesMenuItem,
|
|
3479
|
+
type TableColumn,
|
|
3480
|
+
type TableSortChange,
|
|
3481
|
+
} from '@africanies/africanies-ui';
|
|
3482
|
+
import { filter, firstValueFrom, fromEvent } from 'rxjs';
|
|
3483
|
+
|
|
3484
|
+
interface Shipment {
|
|
3485
|
+
reference: string;
|
|
3486
|
+
status: 'In transit' | 'Delivered' | 'Pending' | 'Exception';
|
|
3487
|
+
mode: 'sfn' | 'stn';
|
|
3488
|
+
destination: string;
|
|
3489
|
+
valueUsd: number;
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
const PAGE_SIZE = DEFAULT_PAGE_SIZE;
|
|
3493
|
+
|
|
3494
|
+
@Component({
|
|
3495
|
+
selector: 'app-shipment-list-page',
|
|
3496
|
+
standalone: true,
|
|
3497
|
+
imports: [
|
|
3498
|
+
TableComponent,
|
|
3499
|
+
CellDefDirective,
|
|
3500
|
+
RowDetailDefDirective,
|
|
3501
|
+
ActionMenuComponent,
|
|
3502
|
+
ChipComponent,
|
|
3503
|
+
CopyButtonComponent,
|
|
3504
|
+
],
|
|
3505
|
+
template: \`
|
|
3506
|
+
<africanies-table
|
|
3507
|
+
[columns]="columns"
|
|
3508
|
+
[rows]="rows()"
|
|
3509
|
+
[meta]="meta()"
|
|
3510
|
+
[sort]="sort()"
|
|
3511
|
+
[rowTrackBy]="rowTrackBy"
|
|
3512
|
+
[showRefresh]="true"
|
|
3513
|
+
[refreshing]="isRefreshing()"
|
|
3514
|
+
[loading]="isLoading() || isPageLoading()"
|
|
3515
|
+
[error]="listError()"
|
|
3516
|
+
emptyMessage="No shipments match these filters."
|
|
3517
|
+
[showFilter]="true"
|
|
3518
|
+
[showExport]="true"
|
|
3519
|
+
(sortChange)="onSort($event)"
|
|
3520
|
+
(refreshClick)="refetch({ reason: 'refresh' })"
|
|
3521
|
+
(filterClick)="openFilters()"
|
|
3522
|
+
(exportClick)="exportCsv()"
|
|
3523
|
+
(pageChange)="onPageChange($event)"
|
|
3524
|
+
(sizeChange)="onSizeChange($event)"
|
|
3525
|
+
>
|
|
3526
|
+
<ng-template africaniesCellDef="reference" let-row>
|
|
3527
|
+
<div class="flex items-center gap-1">
|
|
3528
|
+
<span class="font-medium">{{ row.reference }}</span>
|
|
3529
|
+
<africanies-copy
|
|
3530
|
+
[value]="row.reference"
|
|
3531
|
+
[ariaLabel]="'Copy ' + row.reference"
|
|
3532
|
+
/>
|
|
3533
|
+
</div>
|
|
3534
|
+
</ng-template>
|
|
3535
|
+
|
|
3536
|
+
<ng-template africaniesCellDef="status" let-row>
|
|
3537
|
+
<africanies-chip [variant]="statusVariant(row.status)">{{ row.status }}</africanies-chip>
|
|
3538
|
+
</ng-template>
|
|
3539
|
+
|
|
3540
|
+
<ng-template africaniesCellDef="valueUsd" let-row>
|
|
3541
|
+
<span class="tabular-nums">{{ formatUsd(row.valueUsd) }}</span>
|
|
3542
|
+
</ng-template>
|
|
3543
|
+
|
|
3544
|
+
<ng-template africaniesCellDef="actions" let-row>
|
|
3545
|
+
<africanies-action-menu
|
|
3546
|
+
[items]="rowActions(row)"
|
|
3547
|
+
[ariaLabel]="'Actions for ' + row.reference"
|
|
3548
|
+
/>
|
|
3549
|
+
</ng-template>
|
|
3550
|
+
|
|
3551
|
+
<ng-template africaniesRowDetail="Destination" let-row>
|
|
3552
|
+
<span class="font-medium">{{ row.destination }}</span>
|
|
3553
|
+
</ng-template>
|
|
3554
|
+
|
|
3555
|
+
<ng-template africaniesRowDetail="Declared value" let-row>
|
|
3556
|
+
<span class="tabular-nums">{{ formatUsd(row.valueUsd) }}</span>
|
|
3557
|
+
</ng-template>
|
|
3558
|
+
|
|
3559
|
+
<ng-template africaniesRowDetail="Status" let-row>
|
|
3560
|
+
<africanies-chip [variant]="statusVariant(row.status)">{{ row.status }}</africanies-chip>
|
|
3561
|
+
</ng-template>
|
|
3562
|
+
</africanies-table>
|
|
3563
|
+
\`,
|
|
3564
|
+
})
|
|
3565
|
+
export class ShipmentListPageComponent {
|
|
3566
|
+
private readonly api = inject(ApiClient);
|
|
3567
|
+
private readonly shipping = inject(ShippingModeService);
|
|
3568
|
+
private readonly filterQuery = inject(FilterQueryService);
|
|
3569
|
+
private readonly urlState = this.filterQuery.read(trackShipmentsFilterConfig);
|
|
3570
|
+
|
|
3571
|
+
protected readonly page = signal(this.urlState.page ?? 1);
|
|
3572
|
+
protected readonly size = signal(this.urlState.size ?? PAGE_SIZE);
|
|
3573
|
+
protected readonly sort = signal<TableSortChange | null>(null);
|
|
3574
|
+
protected readonly rows = signal<Shipment[]>([]);
|
|
3575
|
+
protected readonly meta = signal<PaginationMetaModel | null>(null);
|
|
3576
|
+
protected readonly fetchError = signal<string | null>(null);
|
|
3577
|
+
protected readonly isLoading = signal(false);
|
|
3578
|
+
protected readonly isRefreshing = signal(false);
|
|
3579
|
+
protected readonly isPageLoading = signal(false);
|
|
3580
|
+
|
|
3581
|
+
protected readonly columns: TableColumn<Shipment>[] = [
|
|
3582
|
+
{ key: 'reference', header: 'Reference', sortable: true },
|
|
3583
|
+
{ key: 'status', header: 'Status', sortable: true },
|
|
3584
|
+
{ key: 'mode', header: 'Mode' },
|
|
3585
|
+
{ key: 'destination', header: 'Destination', sortable: true },
|
|
3586
|
+
{ key: 'valueUsd', header: 'Value', sortable: true },
|
|
3587
|
+
{ key: 'actions', header: '', width: '3.5rem' },
|
|
3588
|
+
];
|
|
3589
|
+
|
|
3590
|
+
protected readonly rowTrackBy = (row: Shipment) => row.reference;
|
|
3591
|
+
|
|
3592
|
+
protected rowActions(row: Shipment): AfricaniesMenuItem[] {
|
|
3593
|
+
return [
|
|
3594
|
+
{
|
|
3595
|
+
label: 'Open',
|
|
3596
|
+
icon: 'eye',
|
|
3597
|
+
onClick: () => this.open(row),
|
|
3598
|
+
},
|
|
3599
|
+
{
|
|
3600
|
+
label: 'Edit',
|
|
3601
|
+
icon: 'edit',
|
|
3602
|
+
onClick: () => this.edit(row),
|
|
3603
|
+
},
|
|
3604
|
+
{
|
|
3605
|
+
label: 'Copy reference',
|
|
3606
|
+
icon: 'copy',
|
|
3607
|
+
onClick: () => this.copyReference(row),
|
|
3608
|
+
},
|
|
3609
|
+
{
|
|
3610
|
+
label: 'Delete',
|
|
3611
|
+
icon: 'trash',
|
|
3612
|
+
danger: true,
|
|
3613
|
+
dividerBefore: true,
|
|
3614
|
+
onClick: () => this.delete(row),
|
|
3615
|
+
},
|
|
3616
|
+
];
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
protected readonly listState = computed((): AsyncQueryStateModel<Shipment[]> => ({
|
|
3620
|
+
data: this.rows(),
|
|
3621
|
+
isLoading: this.isLoading(),
|
|
3622
|
+
isFetching: this.isRefreshing() || this.isPageLoading(),
|
|
3623
|
+
isError: this.fetchError() != null,
|
|
3624
|
+
error: this.fetchError(),
|
|
3625
|
+
}));
|
|
3626
|
+
|
|
3627
|
+
constructor() {
|
|
3628
|
+
effect(() => {
|
|
3629
|
+
this.shipping.mode();
|
|
3630
|
+
untracked(() =>
|
|
3631
|
+
void this.refetch({
|
|
3632
|
+
reason: this.rows().length > 0 ? 'mode' : 'initial',
|
|
3633
|
+
}),
|
|
3634
|
+
);
|
|
3635
|
+
});
|
|
3636
|
+
|
|
3637
|
+
fromEvent(document, 'visibilitychange')
|
|
3638
|
+
.pipe(
|
|
3639
|
+
filter(() => document.visibilityState === 'visible'),
|
|
3640
|
+
takeUntilDestroyed(),
|
|
3641
|
+
)
|
|
3642
|
+
.subscribe(() => void this.refetch({ reason: 'focus' }));
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
protected async refetch(
|
|
3646
|
+
opts: { reason: ListFetchReason } = { reason: 'initial' },
|
|
3647
|
+
): Promise<void> {
|
|
3648
|
+
const kind = listFetchKind({
|
|
3649
|
+
hasData: this.rows().length > 0,
|
|
3650
|
+
reason: opts.reason,
|
|
3651
|
+
});
|
|
3652
|
+
this.isLoading.set(kind === 'loading');
|
|
3653
|
+
this.isRefreshing.set(kind === 'refreshing');
|
|
3654
|
+
this.isPageLoading.set(kind === 'pagination');
|
|
3655
|
+
if (kind === 'loading') {
|
|
3656
|
+
this.rows.set([]);
|
|
3657
|
+
}
|
|
3658
|
+
this.fetchError.set(null);
|
|
3659
|
+
try {
|
|
3660
|
+
const currentSort = this.sort();
|
|
3661
|
+
const res = await firstValueFrom(
|
|
3662
|
+
this.api.getResource<Shipment>('shipments', null, {
|
|
3663
|
+
page: this.page(),
|
|
3664
|
+
size: this.size(),
|
|
3665
|
+
// Map sort key/direction to your API query params when supported.
|
|
3666
|
+
orderBy: currentSort?.key,
|
|
3667
|
+
order: currentSort?.direction,
|
|
3668
|
+
}),
|
|
3669
|
+
);
|
|
3670
|
+
this.rows.set(res.data ?? []);
|
|
3671
|
+
this.meta.set(res.pagination ?? null);
|
|
3672
|
+
} catch {
|
|
3673
|
+
this.fetchError.set('Could not load shipments.');
|
|
3674
|
+
} finally {
|
|
3675
|
+
this.isLoading.set(false);
|
|
3676
|
+
this.isRefreshing.set(false);
|
|
3677
|
+
this.isPageLoading.set(false);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
protected onSort(change: TableSortChange): void {
|
|
3682
|
+
this.sort.set(change);
|
|
3683
|
+
this.page.set(1);
|
|
3684
|
+
void this.refetch({ reason: 'page' });
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
protected onPageChange(next: number): void {
|
|
3688
|
+
this.page.set(next);
|
|
3689
|
+
void this.refetch({ reason: 'page' });
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
protected onSizeChange(next: number): void {
|
|
3693
|
+
this.size.set(next);
|
|
3694
|
+
this.page.set(1);
|
|
3695
|
+
void this.refetch({ reason: 'page' });
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
private open(row: Shipment): void {
|
|
3699
|
+
/* navigate to detail */
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
private edit(row: Shipment): void {
|
|
3703
|
+
/* open edit flow */
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
private copyReference(row: Shipment): void {
|
|
3707
|
+
/* clipboard.writeText(row.reference) */
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
private delete(row: Shipment): void {
|
|
3711
|
+
/* confirm + API delete */
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
protected exportCsv(): void {
|
|
3715
|
+
downloadCsv({
|
|
3716
|
+
filename: 'shipments.csv',
|
|
3717
|
+
headers: ['Reference', 'Status', 'Destination', 'Value (USD)'],
|
|
3718
|
+
rows: this.rows().map((row) => [
|
|
3719
|
+
row.reference,
|
|
3720
|
+
row.status,
|
|
3721
|
+
row.destination,
|
|
3722
|
+
row.valueUsd,
|
|
3723
|
+
]),
|
|
3724
|
+
});
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
protected formatUsd(value: number): string {
|
|
3728
|
+
return new Intl.NumberFormat('en-US', {
|
|
3729
|
+
style: 'currency',
|
|
3730
|
+
currency: 'USD',
|
|
3731
|
+
maximumFractionDigits: 0,
|
|
3732
|
+
}).format(value);
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
// --- Playground-only demo (in-memory slice \u2014 NOT for production) ---
|
|
3737
|
+
//
|
|
3738
|
+
// const ALL_ROWS: Shipment[] = [/* static fixture */];
|
|
3739
|
+
//
|
|
3740
|
+
// pageRows = computed(() => {
|
|
3741
|
+
// const start = (this.page() - 1) * PAGE_SIZE;
|
|
3742
|
+
// return sortedRows().slice(start, start + PAGE_SIZE);
|
|
3743
|
+
// });
|
|
3744
|
+
//
|
|
3745
|
+
// meta = computed((): PaginationMetaModel => ({ /* derived from ALL_ROWS.length */ }));
|
|
3746
|
+
`,Ao=`// Plain text columns only \u2014 no africaniesCellDef needed; cells render String(row[key]).
|
|
3747
|
+
// Fine for small previews; skip AsyncState/pagination if the set is static.
|
|
3748
|
+
|
|
3749
|
+
import { Component } from '@angular/core';
|
|
3750
|
+
import { TableComponent, type TableColumn } from '@africanies/africanies-ui';
|
|
3751
|
+
|
|
3752
|
+
interface Shipment {
|
|
3753
|
+
reference: string;
|
|
3754
|
+
destination: string;
|
|
3755
|
+
status: string;
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
@Component({
|
|
3759
|
+
selector: 'app-shipment-compact-preview',
|
|
3760
|
+
standalone: true,
|
|
3761
|
+
imports: [TableComponent],
|
|
3762
|
+
template: \`
|
|
3763
|
+
<africanies-table [columns]="columns" [rows]="rows" />
|
|
3764
|
+
\`,
|
|
3765
|
+
})
|
|
3766
|
+
export class ShipmentCompactPreviewComponent {
|
|
3767
|
+
protected readonly columns: TableColumn<Shipment>[] = [
|
|
3768
|
+
{ key: 'reference', header: 'Reference' },
|
|
3769
|
+
{ key: 'destination', header: 'Destination' },
|
|
3770
|
+
{ key: 'status', header: 'Status' },
|
|
3771
|
+
];
|
|
3772
|
+
|
|
3773
|
+
protected readonly rows: Shipment[] = [
|
|
3774
|
+
{ reference: 'SFN-1042', destination: 'Lagos', status: 'In transit' },
|
|
3775
|
+
{ reference: 'STN-2088', destination: 'Accra', status: 'Delivered' },
|
|
3776
|
+
{ reference: 'SFN-1099', destination: 'Nairobi', status: 'Pending' },
|
|
3777
|
+
];
|
|
3778
|
+
}
|
|
3779
|
+
`,ko=`// Stacked title / subtitle / extra line. Drop into a table cell (or a detail panel).
|
|
3780
|
+
// The table row grows to fit \u2014 no extra layout work.
|
|
3781
|
+
|
|
3782
|
+
import { Component } from '@angular/core';
|
|
3783
|
+
import { ContentStackComponent } from '@africanies/africanies-ui';
|
|
3784
|
+
|
|
3785
|
+
@Component({
|
|
3786
|
+
selector: 'app-packaged-by',
|
|
3787
|
+
standalone: true,
|
|
3788
|
+
imports: [ContentStackComponent],
|
|
3789
|
+
template: \`
|
|
3790
|
+
<africanies-content-stack
|
|
3791
|
+
title="Oladotun Adedeji"
|
|
3792
|
+
subtitle="oladotun.a@africanies.com"
|
|
3793
|
+
extraLine="Aug 14, 2026, 2:04:22 PM"
|
|
3794
|
+
/>
|
|
3795
|
+
\`,
|
|
3796
|
+
})
|
|
3797
|
+
export class PackagedByComponent {}
|
|
3798
|
+
`,_o=`// Customer list: switch columns by account type (individual vs business).
|
|
3799
|
+
// User: date created, customer, attempts, identifier, status, performed by, action.
|
|
3800
|
+
// Business: date created, business, attempts, company type, incorp no, incorp date, website, status, action.
|
|
3801
|
+
|
|
3802
|
+
protected readonly userColumns: TableColumn<CustomerRow>[] = [
|
|
3803
|
+
{ key: 'createdAt', header: 'Date created', sortable: true },
|
|
3804
|
+
{ key: 'customer', header: 'Customer' },
|
|
3805
|
+
{ key: 'attempts', header: 'Attempts', sortable: true },
|
|
3806
|
+
{ key: 'identifier', header: 'Identifier' },
|
|
3807
|
+
{ key: 'status', header: 'Status', sortable: true },
|
|
3808
|
+
{ key: 'performedBy', header: 'Performed by' },
|
|
3809
|
+
{ key: 'actions', header: 'Action', width: '3.5rem' },
|
|
3810
|
+
];
|
|
3811
|
+
|
|
3812
|
+
protected readonly businessColumns: TableColumn<CustomerRow>[] = [
|
|
3813
|
+
{ key: 'createdAt', header: 'Date created', sortable: true },
|
|
3814
|
+
{ key: 'business', header: 'Business' },
|
|
3815
|
+
{ key: 'attempts', header: 'Attempts', sortable: true },
|
|
3816
|
+
{ key: 'companyType', header: 'Company type' },
|
|
3817
|
+
{ key: 'incorpNo', header: 'Incorp no' },
|
|
3818
|
+
{ key: 'incorpDate', header: 'Incorp date' },
|
|
3819
|
+
{ key: 'website', header: 'Website' },
|
|
3820
|
+
{ key: 'status', header: 'Status', sortable: true },
|
|
3821
|
+
{ key: 'actions', header: 'Action', width: '3.5rem' },
|
|
3822
|
+
];
|
|
3823
|
+
`;var Mo=`
|
|
3824
|
+
// Inject ToastService and fire away.
|
|
3825
|
+
// Toasts mount via CDK Overlay in the corner (provideAfricaniesToasts() once).
|
|
3826
|
+
// Error stays until closed; warning ~8s; info/success ~4.5s. Hover pauses.
|
|
3827
|
+
|
|
3828
|
+
import { Component, inject } from '@angular/core';
|
|
3829
|
+
import { ButtonComponent, ToastService } from '@africanies/africanies-ui';
|
|
3830
|
+
|
|
3831
|
+
@Component({
|
|
3832
|
+
selector: 'app-toast-demo',
|
|
3833
|
+
standalone: true,
|
|
3834
|
+
imports: [ButtonComponent],
|
|
3835
|
+
template: \`
|
|
3836
|
+
<button africanies-button type="button" (click)="ok()">Saved</button>
|
|
3837
|
+
<button africanies-button type="button" variant="secondary" (click)="warn()">Warn</button>
|
|
3838
|
+
<button africanies-button type="button" variant="danger" (click)="fail()">Error</button>
|
|
3839
|
+
\`,
|
|
3840
|
+
})
|
|
3841
|
+
export class ToastDemoComponent {
|
|
3842
|
+
private readonly toast = inject(ToastService);
|
|
3843
|
+
|
|
3844
|
+
protected ok(): void {
|
|
3845
|
+
this.toast.success('Shipment saved', 'Done');
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
protected warn(): void {
|
|
3849
|
+
this.toast.warning('Rates may be outdated. Refresh before quoting.');
|
|
3850
|
+
}
|
|
3851
|
+
|
|
3852
|
+
protected fail(): void {
|
|
3853
|
+
this.toast.error('Could not reach the carrier API.', 'Request failed');
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
`,Io=`
|
|
3857
|
+
// Same message twice? They collapse into one with \xD7N.
|
|
3858
|
+
// X peels one copy; Expand shows all; Close all clears the group.
|
|
3859
|
+
|
|
3860
|
+
this.toast.error('Warehouse offline');
|
|
3861
|
+
this.toast.error('Warehouse offline'); // \xD72
|
|
3862
|
+
`,No=`
|
|
3863
|
+
// Wrap HttpClient calls with withToast(). Success + error on by default.
|
|
3864
|
+
// Needs provideAfricaniesHttpClient() + provideAfricaniesToasts() in app.config.
|
|
3865
|
+
|
|
3866
|
+
import { HttpClient } from '@angular/common/http';
|
|
3867
|
+
import { inject } from '@angular/core';
|
|
3868
|
+
import { withToast } from '@africanies/africanies-core';
|
|
3869
|
+
|
|
3870
|
+
const http = inject(HttpClient);
|
|
3871
|
+
|
|
3872
|
+
// Success toast + persistent error toast
|
|
3873
|
+
http.post('/shipments', body, { context: withToast() }).subscribe();
|
|
3874
|
+
|
|
3875
|
+
// Errors only
|
|
3876
|
+
http.post('/shipments', body, {
|
|
3877
|
+
context: withToast({ success: false }),
|
|
3878
|
+
}).subscribe();
|
|
3879
|
+
|
|
3880
|
+
// Custom copy
|
|
3881
|
+
http.post('/shipments', body, {
|
|
3882
|
+
context: withToast({
|
|
3883
|
+
successMessage: 'Shipment created',
|
|
3884
|
+
errorMessage: 'Could not create shipment',
|
|
3885
|
+
}),
|
|
3886
|
+
}).subscribe();
|
|
3887
|
+
`;var Oo=`
|
|
3888
|
+
// Help next to a label. Ghost info icon; opens on hover, focus, or tap.
|
|
3889
|
+
// No overlay provider needed \u2014 uses CDK like select / action-menu.
|
|
3890
|
+
|
|
3891
|
+
import { TooltipComponent } from '@africanies/africanies-ui';
|
|
3892
|
+
|
|
3893
|
+
@Component({
|
|
3894
|
+
imports: [TooltipComponent],
|
|
3895
|
+
template: \`
|
|
3896
|
+
<div class="inline-flex items-center gap-1.5">
|
|
3897
|
+
Insured
|
|
3898
|
+
<africanies-tooltip
|
|
3899
|
+
text="Coverage against loss or damage while the shipment is in transit."
|
|
3900
|
+
/>
|
|
3901
|
+
</div>
|
|
3902
|
+
\`,
|
|
3903
|
+
})
|
|
3904
|
+
export class Example {}
|
|
3905
|
+
`,Do=`
|
|
3906
|
+
// Wrap any control and mark it with africaniesTooltipTrigger \u2014 default info icon hides.
|
|
3907
|
+
|
|
3908
|
+
import { ButtonComponent, TooltipComponent, TooltipTriggerDirective } from '@africanies/africanies-ui';
|
|
3909
|
+
|
|
3910
|
+
@Component({
|
|
3911
|
+
imports: [ButtonComponent, TooltipComponent, TooltipTriggerDirective],
|
|
3912
|
+
template: \`
|
|
3913
|
+
<africanies-tooltip text="Required when the declared value exceeds $2,000.">
|
|
3914
|
+
<button
|
|
3915
|
+
type="button"
|
|
3916
|
+
africanies-button
|
|
3917
|
+
africaniesTooltipTrigger
|
|
3918
|
+
variant="ghost"
|
|
3919
|
+
size="sm"
|
|
3920
|
+
>
|
|
3921
|
+
Why is this required?
|
|
3922
|
+
</button>
|
|
3923
|
+
</africanies-tooltip>
|
|
3924
|
+
\`,
|
|
3925
|
+
})
|
|
3926
|
+
export class Example {}
|
|
3927
|
+
`,Po=`
|
|
3928
|
+
// placement prefers a side; CDK flips if there\u2019s no room.
|
|
3929
|
+
// 'top' | 'bottom' | 'left' | 'right' (default 'top').
|
|
3930
|
+
|
|
3931
|
+
<africanies-tooltip text="Opens above when possible." placement="top" />
|
|
3932
|
+
<africanies-tooltip text="Opens below when possible." placement="bottom" />
|
|
3933
|
+
<africanies-tooltip text="Opens to the left." placement="left" />
|
|
3934
|
+
<africanies-tooltip text="Opens to the right." placement="right" />
|
|
3935
|
+
`,Lo=`
|
|
3936
|
+
// Label + tip beside it \u2014 don\u2019t wrap the input itself.
|
|
3937
|
+
|
|
3938
|
+
import { TextInputComponent, TooltipComponent } from '@africanies/africanies-ui';
|
|
3939
|
+
|
|
3940
|
+
@Component({
|
|
3941
|
+
imports: [TextInputComponent, TooltipComponent],
|
|
3942
|
+
template: \`
|
|
3943
|
+
<div class="flex flex-col gap-1.5">
|
|
3944
|
+
<div class="flex items-center gap-1.5">
|
|
3945
|
+
<span class="text-body-sm font-medium text-ink dark:text-white">
|
|
3946
|
+
Tracking number
|
|
3947
|
+
</span>
|
|
3948
|
+
<africanies-tooltip
|
|
3949
|
+
text="Carrier AWB or internal shipment reference used on the warehouse floor."
|
|
3950
|
+
icon="question-circle"
|
|
3951
|
+
ariaLabel="About tracking number"
|
|
3952
|
+
/>
|
|
3953
|
+
</div>
|
|
3954
|
+
<africanies-text-input label="" placeholder="SFN-\u2026" [(value)]="tracking" />
|
|
3955
|
+
</div>
|
|
3956
|
+
\`,
|
|
3957
|
+
})
|
|
3958
|
+
export class Example {
|
|
3959
|
+
tracking = '';
|
|
3960
|
+
}
|
|
3961
|
+
`;var Bo=`// No Back button or breadcrumbs to build. africanies-app-shell shows both in the
|
|
3962
|
+
// content chrome. Back appears only on child routes (and still does after a
|
|
3963
|
+
// pasted / reloaded URL). Nested children under a parent is all the app needs.
|
|
3964
|
+
//
|
|
3965
|
+
// List filters + pagination live on the query string (FilterQueryService).
|
|
3966
|
+
// Open detail with queryParamsHandling: 'preserve' so Back restores page/filters.
|
|
3967
|
+
|
|
3968
|
+
import { FilterQueryService, trackShipmentsFilterConfig } from '@africanies/africanies-ui';
|
|
3969
|
+
|
|
3970
|
+
const routes: Route[] = [
|
|
3971
|
+
{
|
|
3972
|
+
path: 'usecases/shipment',
|
|
3973
|
+
loadComponent: () =>
|
|
3974
|
+
import('./shipment-usecase.page').then((m) => m.ShipmentUsecasePage),
|
|
3975
|
+
children: [
|
|
3976
|
+
{
|
|
3977
|
+
path: '',
|
|
3978
|
+
loadComponent: () =>
|
|
3979
|
+
import('./shipment-list.page').then((m) => m.ShipmentListPage),
|
|
3980
|
+
},
|
|
3981
|
+
{
|
|
3982
|
+
path: ':id',
|
|
3983
|
+
loadComponent: () =>
|
|
3984
|
+
import('./shipment-detail.page').then((m) => m.ShipmentDetailPage),
|
|
3985
|
+
},
|
|
3986
|
+
],
|
|
3987
|
+
},
|
|
3988
|
+
];
|
|
3989
|
+
|
|
3990
|
+
// List page \u2014 seed from the URL, then let Apply / africanies-pagination write back:
|
|
3991
|
+
const filterQuery = inject(FilterQueryService);
|
|
3992
|
+
const config = trackShipmentsFilterConfig;
|
|
3993
|
+
const state = filterQuery.hasParams(config)
|
|
3994
|
+
? filterQuery.read(config)
|
|
3995
|
+
: emptyFilterState();
|
|
3996
|
+
|
|
3997
|
+
// Detail navigation keeps the list queries on the URL:
|
|
3998
|
+
this.router.navigate(['/usecases/shipment', row.reference], {
|
|
3999
|
+
queryParamsHandling: 'preserve',
|
|
4000
|
+
});
|
|
4001
|
+
`,Uo=`// Forgot password is email-only. Login \u2192 /onboarding/forgot-password.
|
|
4002
|
+
// Submit POSTs { email } to /auth/forgot/password. This app does not handle
|
|
4003
|
+
// the emailed link (no token in the route). /onboarding/reset-password is a
|
|
4004
|
+
// different flow: first login when user.default_password is set.
|
|
4005
|
+
|
|
4006
|
+
import { Component, computed, inject, signal } from '@angular/core';
|
|
4007
|
+
import { RouterLink } from '@angular/router';
|
|
4008
|
+
import { AuthService, isValidEmail } from '@africanies/africanies-core';
|
|
4009
|
+
import { firstValueFrom } from 'rxjs';
|
|
4010
|
+
|
|
4011
|
+
const routes: Route[] = [
|
|
4012
|
+
{ path: 'onboarding/login', loadComponent: () => import('./login.page') },
|
|
4013
|
+
{
|
|
4014
|
+
path: 'onboarding/forgot-password',
|
|
4015
|
+
loadComponent: () => import('./forgot-password.page'),
|
|
4016
|
+
},
|
|
4017
|
+
// NOT part of forgot-password \u2014 first login with a default password:
|
|
4018
|
+
{
|
|
4019
|
+
path: 'onboarding/reset-password',
|
|
4020
|
+
loadComponent: () => import('./reset-password.page'),
|
|
4021
|
+
},
|
|
4022
|
+
];
|
|
4023
|
+
|
|
4024
|
+
@Component({
|
|
4025
|
+
selector: 'app-forgot-password',
|
|
4026
|
+
standalone: true,
|
|
4027
|
+
template: \`
|
|
4028
|
+
<p>We will send you a password reset link.</p>
|
|
4029
|
+
<africanies-text-input type="email" label="Email" [(value)]="email" />
|
|
4030
|
+
<button
|
|
4031
|
+
africanies-button
|
|
4032
|
+
type="button"
|
|
4033
|
+
[disabled]="!canSubmit()"
|
|
4034
|
+
[loading]="sending()"
|
|
4035
|
+
(click)="forgot()"
|
|
4036
|
+
>
|
|
4037
|
+
Send reset link
|
|
4038
|
+
</button>
|
|
4039
|
+
@if (message(); as copy) {
|
|
4040
|
+
<p>{{ copy }}</p>
|
|
4041
|
+
<a routerLink="/onboarding/login">Reset completed? Login here</a>
|
|
4042
|
+
}
|
|
4043
|
+
\`,
|
|
4044
|
+
})
|
|
4045
|
+
export class ForgotPasswordPage {
|
|
4046
|
+
private readonly authApi = inject(AuthService);
|
|
4047
|
+
protected readonly email = signal('');
|
|
4048
|
+
protected readonly sending = signal(false);
|
|
4049
|
+
protected readonly message = signal<string | null>(null);
|
|
4050
|
+
protected readonly canSubmit = computed(() => isValidEmail(this.email()));
|
|
4051
|
+
|
|
4052
|
+
protected async forgot(): Promise<void> {
|
|
4053
|
+
if (!this.canSubmit()) return;
|
|
4054
|
+
this.sending.set(true);
|
|
4055
|
+
try {
|
|
4056
|
+
const res = await firstValueFrom(this.authApi.forgot(this.email()));
|
|
4057
|
+
if (res.success) {
|
|
4058
|
+
this.message.set(res.message?.trim() ?? null);
|
|
4059
|
+
}
|
|
4060
|
+
} finally {
|
|
4061
|
+
this.sending.set(false);
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
// After login:
|
|
4067
|
+
// if (user.default_password) router.navigate(['/onboarding/reset-password']);
|
|
4068
|
+
// reset-password calls UserService.changePassword({ current_password, password, password_confirmation })
|
|
4069
|
+
// Admins can also call authApi.forgot(user.email) from user/partner screens.
|
|
4070
|
+
`;export{$r as $,Ar as A,Bi as B,Cr as C,Dr as D,Er as E,Fr as F,Gr as G,Ht as H,Ir as I,Po as J,Kr as K,Lr as L,Mr as M,Nr as N,Or as O,Pr as P,Lo as Q,Rr as R,Sr as S,Mo as T,Ui as U,Vi as V,Wr as W,Io as X,Yr as Y,No as Z,_r as _,Hi as a,_o as a0,Eo as a1,Ao as a2,ko as a3,Co as a4,xo as a5,co as a6,po as a7,mo as a8,uo as a9,rr as aA,or as aB,Ji as aC,sr as aD,fo as aa,go as ab,Bo as ac,Uo as ad,oo as ae,Qr as af,io as ag,ro as ah,no as ai,to as aj,eo as ak,Jr as al,Xr as am,so as an,qi as ao,$i as ap,Ki as aq,Yi as ar,Zi as as,Qi as at,Xi as au,er as av,tr as aw,nr as ax,ar as ay,ir as az,br as b,cr as c,dr as d,bo as e,fr as f,gr as g,hr as h,vo as i,ji as j,kr as k,jr as l,mr as m,zr as n,Vr as o,pr as p,qr as q,Hr as r,Br as s,Oo as t,ur as u,vr as v,wr as w,Do as x,yo as y,zi as z};
|