dcc 0.2.0 → 0.2.1
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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +19 -0
- data/.github/workflows/release.yml +28 -0
- data/.gitignore +4 -0
- data/.rubocop_todo.yml +560 -10
- data/CHANGELOG.adoc +34 -1
- data/CONTRIBUTING.adoc +8 -1
- data/Gemfile +5 -7
- data/README.adoc +162 -7
- data/dcc.gemspec +8 -2
- data/docs/schema-versions.adoc +20 -7
- data/examples/build_minimal_dcc.rb +0 -0
- data/examples/convert_all_formats.rb +2 -1
- data/examples/parse_and_inspect.rb +0 -0
- data/examples/quantity_math_demo.rb +7 -7
- data/examples/validate_full.rb +1 -1
- data/lib/dcc/base/calibration_laboratory.rb +2 -1
- data/lib/dcc/base/condition.rb +5 -2
- data/lib/dcc/base/core_data.rb +8 -3
- data/lib/dcc/base/formula.rb +3 -3
- data/lib/dcc/base/identification.rb +6 -3
- data/lib/dcc/base/mathml.rb +33 -0
- data/lib/dcc/base/quantity.rb +2 -1
- data/lib/dcc/base/resp_person.rb +4 -2
- data/lib/dcc/base/text.rb +1 -1
- data/lib/dcc/base.rb +2 -1
- data/lib/dcc/builder/dsl.rb +1 -1
- data/lib/dcc/builder/session.rb +16 -14
- data/lib/dcc/builder.rb +3 -3
- data/lib/dcc/cli/cli.rb +36 -18
- data/lib/dcc/cli/formatters.rb +35 -7
- data/lib/dcc/cli.rb +1 -1
- data/lib/dcc/context_configuration.rb +2 -1
- data/lib/dcc/context_options.rb +4 -2
- data/lib/dcc/convert/csv.rb +27 -9
- data/lib/dcc/convert/html.rb +26 -110
- data/lib/dcc/convert/json.rb +10 -7
- data/lib/dcc/convert/liquid_filters.rb +58 -0
- data/lib/dcc/convert/result.rb +1 -1
- data/lib/dcc/convert/templates/report.liquid +250 -0
- data/lib/dcc/convert/yaml.rb +1 -1
- data/lib/dcc/convert.rb +1 -0
- data/lib/dcc/diff/change.rb +1 -1
- data/lib/dcc/diff/result.rb +8 -4
- data/lib/dcc/diff.rb +43 -11
- data/lib/dcc/error.rb +7 -0
- data/lib/dcc/extract/file.rb +3 -4
- data/lib/dcc/extract/ring.rb +1 -1
- data/lib/dcc/extract.rb +1 -1
- data/lib/dcc/i18n/text_lookup.rb +8 -4
- data/lib/dcc/i18n.rb +1 -1
- data/lib/dcc/inspect/report.rb +51 -14
- data/lib/dcc/inspect.rb +1 -1
- data/lib/dcc/migrate/route.rb +44 -0
- data/lib/dcc/migrate/v2_to_v3.rb +445 -0
- data/lib/dcc/migrate.rb +130 -26
- data/lib/dcc/plugin/base.rb +64 -0
- data/lib/dcc/plugin/registry.rb +7 -2
- data/lib/dcc/plugin.rb +6 -2
- data/lib/dcc/quantity_format/formatter.rb +16 -10
- data/lib/dcc/quantity_format.rb +1 -1
- data/lib/dcc/quantity_math/quantity.rb +1 -1
- data/lib/dcc/quantity_math/real.rb +6 -6
- data/lib/dcc/quantity_math.rb +1 -1
- data/lib/dcc/schema/version.rb +2 -2
- data/lib/dcc/server/app.html +282 -0
- data/lib/dcc/server/app.rb +171 -87
- data/lib/dcc/server/demo_cert.xml +244 -0
- data/lib/dcc/server/storage/entry.rb +1 -1
- data/lib/dcc/server/storage/memory.rb +3 -2
- data/lib/dcc/server/storage.rb +1 -1
- data/lib/dcc/server.rb +1 -1
- data/lib/dcc/si/base/complex.rb +1 -1
- data/lib/dcc/si/base/complex_list_xml_list.rb +1 -1
- data/lib/dcc/si/base/constant.rb +1 -1
- data/lib/dcc/si/base/coverage_interval.rb +36 -0
- data/lib/dcc/si/base/coverage_interval_mu.rb +1 -1
- data/lib/dcc/si/base/expanded_mu.rb +1 -1
- data/lib/dcc/si/base/expanded_unc.rb +34 -0
- data/lib/dcc/si/base/expanded_unc_xml_list.rb +3 -2
- data/lib/dcc/si/base/hybrid.rb +3 -2
- data/lib/dcc/si/base/measurement_uncertainty_univariate.rb +1 -1
- data/lib/dcc/si/base/real.rb +13 -3
- data/lib/dcc/si/base/real_list_xml_list.rb +5 -3
- data/lib/dcc/si/base/si_list.rb +3 -2
- data/lib/dcc/si/base/standard_mu.rb +1 -1
- data/lib/dcc/si/base.rb +5 -2
- data/lib/dcc/si/v1/coverage_interval.rb +8 -0
- data/lib/dcc/si/v1/expanded_unc.rb +1 -16
- data/lib/dcc/si/v1/measurement_uncertainty_univariate.rb +2 -1
- data/lib/dcc/si/v1.rb +5 -2
- data/lib/dcc/si/v2/coverage_interval.rb +8 -0
- data/lib/dcc/si/v2/coverage_interval_mu_xml_list.rb +4 -2
- data/lib/dcc/si/v2/expanded_mu_xml_list.rb +2 -1
- data/lib/dcc/si/v2/expanded_unc.rb +8 -0
- data/lib/dcc/si/v2/measurement_uncertainty_univariate.rb +2 -1
- data/lib/dcc/si/v2.rb +8 -3
- data/lib/dcc/signature/result.rb +2 -2
- data/lib/dcc/signature/signer.rb +31 -18
- data/lib/dcc/signature/verifier.rb +23 -21
- data/lib/dcc/signature.rb +1 -1
- data/lib/dcc/transform/result.rb +1 -1
- data/lib/dcc/transform/xslt.rb +22 -12
- data/lib/dcc/transform.rb +5 -4
- data/lib/dcc/type/base64_binary.rb +5 -3
- data/lib/dcc/type/decimal_xml_list.rb +99 -13
- data/lib/dcc/type/iso_country_code.rb +3 -2
- data/lib/dcc/type/iso_language_code.rb +3 -2
- data/lib/dcc/type/schema_version.rb +3 -2
- data/lib/dcc/type/si_unit.rb +3 -2
- data/lib/dcc/type_guards.rb +3 -3
- data/lib/dcc/v2/calibration_laboratory.rb +2 -1
- data/lib/dcc/v2/condition.rb +22 -1
- data/lib/dcc/v2/configuration.rb +1 -1
- data/lib/dcc/v2/digital_calibration_certificate.rb +2 -1
- data/lib/dcc/v2/identification.rb +14 -1
- data/lib/dcc/v2/mathml.rb +8 -0
- data/lib/dcc/v2/measurement_meta_data_list.rb +2 -1
- data/lib/dcc/v2/quantity.rb +4 -2
- data/lib/dcc/v2.rb +4 -2
- data/lib/dcc/v3/calibration_laboratory.rb +2 -1
- data/lib/dcc/v3/configuration.rb +1 -1
- data/lib/dcc/v3/digital_calibration_certificate.rb +2 -1
- data/lib/dcc/v3/mathml.rb +8 -0
- data/lib/dcc/v3/measurement_meta_data_list.rb +2 -1
- data/lib/dcc/v3/quantity.rb +4 -2
- data/lib/dcc/v3.rb +12 -2
- data/lib/dcc/validate/business_rules.rb +23 -9
- data/lib/dcc/validate/issue.rb +2 -1
- data/lib/dcc/validate/result.rb +6 -4
- data/lib/dcc/validate/schematron/profile.rb +73 -18
- data/lib/dcc/validate/schematron/rule.rb +13 -3
- data/lib/dcc/validate/schematron/rules/administrative_data_completeness.rb +11 -4
- data/lib/dcc/validate/schematron/rules/base.rb +1 -2
- data/lib/dcc/validate/schematron/rules/date_range_check.rb +11 -5
- data/lib/dcc/validate/schematron/rules/id_refid_linking.rb +0 -3
- data/lib/dcc/validate/schematron/rules/influence_conditions_placement.rb +22 -12
- data/lib/dcc/validate/schematron/rules/iso_code_validation.rb +31 -14
- data/lib/dcc/validate/schematron/rules/language_code_dedup.rb +12 -5
- data/lib/dcc/validate/schematron/rules/non_si_declaration.rb +7 -4
- data/lib/dcc/validate/schematron/rules/release_format_check.rb +7 -4
- data/lib/dcc/validate/schematron/rules/schema_version_check.rb +13 -9
- data/lib/dcc/validate/schematron/rules/uncertainty_consistency.rb +39 -10
- data/lib/dcc/validate/schematron/rules/unit_format_check.rb +2 -3
- data/lib/dcc/validate/schematron/rules/used_methods_placement.rb +15 -10
- data/lib/dcc/validate/schematron/rules/used_software_placement.rb +15 -10
- data/lib/dcc/validate/schematron/rules/xml_list_spacing.rb +4 -3
- data/lib/dcc/validate/schematron/rules.rb +28 -14
- data/lib/dcc/validate/schematron.rb +1 -1
- data/lib/dcc/validate/xsd.rb +9 -3
- data/lib/dcc/validate.rb +1 -1
- data/lib/dcc/version.rb +1 -1
- data/lib/dcc/versioned_parser.rb +1 -1
- data/lib/dcc.rb +55 -7
- data/sample_dcc.xml +140 -0
- data/web/index.html +12 -0
- data/web/package-lock.json +1599 -0
- data/web/package.json +22 -0
- data/web/src/App.vue +349 -0
- data/web/src/lib/builder.ts +171 -0
- data/web/src/lib/dcc.ts +338 -0
- data/web/src/lib/units.json +5581 -0
- data/web/src/main.ts +5 -0
- data/web/src/style.css +27 -0
- data/web/tsconfig.json +15 -0
- data/web/vite.config.ts +14 -0
- metadata +125 -9
data/web/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dcc-validator",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DCC Digital Calibration Certificate validator and browser",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview --host"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"vue": "^3.5.0",
|
|
13
|
+
"xmldsigjs": "^2.8.7"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
17
|
+
"@vitejs/plugin-vue": "^6.0.8",
|
|
18
|
+
"tailwindcss": "^4.0.0",
|
|
19
|
+
"typescript": "^5.7.0",
|
|
20
|
+
"vite": "^8.1.5"
|
|
21
|
+
}
|
|
22
|
+
}
|
data/web/src/App.vue
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from "vue";
|
|
3
|
+
import { parseDcc, validateDcc, verifySignature, type DccCert, type ValidationResult } from "./lib/dcc";
|
|
4
|
+
import { emptyForm, emptyQuantity, generateDccXml, type BuilderForm } from "./lib/builder";
|
|
5
|
+
import unitData from "./lib/units.json";
|
|
6
|
+
|
|
7
|
+
interface UnitEntry { short: string; name: string; symbol: string; siunitx: string; siBase: boolean }
|
|
8
|
+
interface QuantityType { short: string; name: string; unit_count: number; units: UnitEntry[] }
|
|
9
|
+
interface PrefixEntry { short: string; name: string; symbol: string; power: string; siunitx: string }
|
|
10
|
+
|
|
11
|
+
const allQuantities = (unitData.quantities as QuantityType[])
|
|
12
|
+
.map(q => ({ ...q, units: q.units.filter(u => u.siunitx) }))
|
|
13
|
+
.filter(q => q.units.length > 0)
|
|
14
|
+
.sort((a, b) => {
|
|
15
|
+
const common = ["mass", "length", "temperature", "electric_resistance", "pressure", "voltage", "electric_current", "frequency", "power", "force", "time", "volume", "density", "angle", "energy"];
|
|
16
|
+
const ai = common.indexOf(a.short); const bi = common.indexOf(b.short);
|
|
17
|
+
if (ai >= 0 && bi >= 0) return ai - bi;
|
|
18
|
+
if (ai >= 0) return -1;
|
|
19
|
+
if (bi >= 0) return 1;
|
|
20
|
+
return a.name.localeCompare(b.name);
|
|
21
|
+
});
|
|
22
|
+
const allPrefixes = (unitData.prefixes as PrefixEntry[])
|
|
23
|
+
.filter(p => ["kilo", "hecto", "deca", "deci", "centi", "milli", "micro", "nano", "pico", "femto", "mega", "giga", "tera"].includes(p.short));
|
|
24
|
+
|
|
25
|
+
const quantities = allQuantities;
|
|
26
|
+
|
|
27
|
+
type Mode = "validate" | "create";
|
|
28
|
+
const mode = ref<Mode>("validate");
|
|
29
|
+
|
|
30
|
+
// === Validate ===
|
|
31
|
+
const cert = ref<DccCert | null>(null);
|
|
32
|
+
const validation = ref<ValidationResult>({ ok: true, issues: [] });
|
|
33
|
+
const loading = ref(false);
|
|
34
|
+
const dragActive = ref(false);
|
|
35
|
+
const showXml = ref(false);
|
|
36
|
+
const rawXml = ref("");
|
|
37
|
+
const sigVerifying = ref(false);
|
|
38
|
+
const sigResult = ref<{ verified: boolean; result?: boolean; error?: string } | null>(null);
|
|
39
|
+
const errors = computed(() => validation.value.issues.filter((i) => i.severity === "error"));
|
|
40
|
+
const warnings = computed(() => validation.value.issues.filter((i) => i.severity === "warning"));
|
|
41
|
+
|
|
42
|
+
async function processXml(xml: string) {
|
|
43
|
+
loading.value = true;
|
|
44
|
+
rawXml.value = xml;
|
|
45
|
+
sigResult.value = null;
|
|
46
|
+
try {
|
|
47
|
+
validation.value = validateDcc(xml);
|
|
48
|
+
cert.value = parseDcc(xml);
|
|
49
|
+
if (cert.value?.signature?.present) {
|
|
50
|
+
sigVerifying.value = true;
|
|
51
|
+
try {
|
|
52
|
+
const sig = await verifySignature(xml);
|
|
53
|
+
sigResult.value = { verified: true, result: sig.verificationResult, error: sig.verificationError };
|
|
54
|
+
} catch (e) { sigResult.value = { verified: false, error: (e as Error).message }; }
|
|
55
|
+
sigVerifying.value = false;
|
|
56
|
+
}
|
|
57
|
+
} catch (e) {
|
|
58
|
+
validation.value = { ok: false, issues: [{ severity: "error", message: (e as Error).message }] };
|
|
59
|
+
cert.value = null;
|
|
60
|
+
}
|
|
61
|
+
loading.value = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function onUpload(e: Event) { const f = (e.target as HTMLInputElement).files?.[0]; if (f) f.text().then(processXml); }
|
|
65
|
+
function onDrop(e: DragEvent) { dragActive.value = false; const f = e.dataTransfer?.files?.[0]; if (f) f.text().then(processXml); }
|
|
66
|
+
|
|
67
|
+
function demoForm(): BuilderForm {
|
|
68
|
+
const f = emptyForm();
|
|
69
|
+
f.uniqueId = "R117-PROMASS-F300"; f.country = "NL"; f.languages = "en, de";
|
|
70
|
+
f.itemName = "Promass F 300 Coriolis Flow Meter"; f.itemModel = "Promass F 300 DNx";
|
|
71
|
+
f.manufacturer = "Endress+Hauser Flowtec AG"; f.labName = "NMi Certin B.V.";
|
|
72
|
+
f.labEmail = "info@nmi.nl"; f.labCode = "NL1"; f.signerName = "M. Schmidt, Ph.D.";
|
|
73
|
+
f.signerEmail = "m.schmidt@nmi.nl"; f.customerName = "Endress+Hauser Flowtec AG";
|
|
74
|
+
f.measurements = [{ name: "Mass flow calibration", quantities: [
|
|
75
|
+
{ ...emptyQuantity(), name: "Max mass flow", value: "300", quantityType: "mass_flow", unitLabel: "kg/min", unitSiunitx: "\\kilogram\\per\\minute", uncertainty: "0.15" },
|
|
76
|
+
{ ...emptyQuantity(), name: "Density min", value: "300", quantityType: "density", unitLabel: "kg/m³", unitSiunitx: "\\kilogram\\per\\metre\\tothe{3}", hasUncertainty: false },
|
|
77
|
+
{ ...emptyQuantity(), name: "Density max", value: "1400", quantityType: "density", unitLabel: "kg/m³", unitSiunitx: "\\kilogram\\per\\metre\\tothe{3}", hasUncertainty: false },
|
|
78
|
+
]}];
|
|
79
|
+
return f;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function loadDemo() { processXml(generateDccXml(demoForm())); }
|
|
83
|
+
function initials(name: string): string {
|
|
84
|
+
if (!name) return "?"; const p = name.trim().split(/\s+/);
|
|
85
|
+
return p.length === 1 ? p[0][0].toUpperCase() : (p[0][0] + p[p.length-1][0]).toUpperCase();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// === Create ===
|
|
89
|
+
const form = ref<BuilderForm>(emptyForm());
|
|
90
|
+
const previewXml = ref("");
|
|
91
|
+
const showPreview = ref(false);
|
|
92
|
+
const buildErrors = ref<string[]>([]);
|
|
93
|
+
|
|
94
|
+
function addMeasurement() { form.value.measurements.push({ name: "", quantities: [emptyQuantity()] }); }
|
|
95
|
+
function removeMeasurement(i: number) { form.value.measurements.splice(i, 1); }
|
|
96
|
+
function addQuantity(mi: number) { form.value.measurements[mi].quantities.push(emptyQuantity()); }
|
|
97
|
+
function removeQuantity(mi: number, qi: number) { form.value.measurements[mi].quantities.splice(qi, 1); }
|
|
98
|
+
|
|
99
|
+
function rebuildUnit(q: BuilderQuantity) {
|
|
100
|
+
const qt = quantities.find(qt => qt.short === q.quantityType);
|
|
101
|
+
if (!qt) { q.unitSiunitx = ""; return; }
|
|
102
|
+
const u = qt.units.find(u => u.short === q.unitLabel);
|
|
103
|
+
if (!u) { q.unitSiunitx = ""; return; }
|
|
104
|
+
q.unitSiunitx = u.siunitx;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function onQuantityTypeChange(q: BuilderQuantity) {
|
|
108
|
+
const qt = quantities.find(qt => qt.short === q.quantityType);
|
|
109
|
+
if (qt && qt.units.length > 0) {
|
|
110
|
+
const first = qt.units.find(u => u.siBase) || qt.units[0];
|
|
111
|
+
q.unitLabel = first.short;
|
|
112
|
+
rebuildUnit(q);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function onUnitChange(q: BuilderQuantity) { rebuildUnit(q); }
|
|
117
|
+
|
|
118
|
+
function onCoverageFactorChange(q: BuilderQuantity) {
|
|
119
|
+
q.coverageProbability = q.coverageFactor === "1" ? "0.68" : q.coverageFactor === "2" ? "0.95" : q.coverageFactor === "3" ? "0.99" : "0.95";
|
|
120
|
+
}
|
|
121
|
+
function generatePreview() {
|
|
122
|
+
previewXml.value = generateDccXml(form.value);
|
|
123
|
+
buildErrors.value = validateDcc(previewXml.value).issues.filter(i => i.severity === "error").map(i => i.message);
|
|
124
|
+
showPreview.value = true;
|
|
125
|
+
}
|
|
126
|
+
function downloadXml() {
|
|
127
|
+
const xml = previewXml.value || generateDccXml(form.value);
|
|
128
|
+
const blob = new Blob([xml], { type: "application/xml" });
|
|
129
|
+
const url = URL.createObjectURL(blob);
|
|
130
|
+
const a = document.createElement("a"); a.href = url;
|
|
131
|
+
a.download = (form.value.uniqueId || "certificate").replace(/[^a-zA-Z0-9-]/g, "_") + ".xml";
|
|
132
|
+
a.click(); URL.revokeObjectURL(url);
|
|
133
|
+
}
|
|
134
|
+
function loadDemoForm() { form.value = demoForm(); }
|
|
135
|
+
</script>
|
|
136
|
+
|
|
137
|
+
<template>
|
|
138
|
+
<div class="min-h-screen bg-slate-950 text-slate-200 font-sans antialiased">
|
|
139
|
+
<!-- Top bar -->
|
|
140
|
+
<div class="border-b border-slate-800 bg-slate-900/50 backdrop-blur sticky top-0 z-50">
|
|
141
|
+
<div class="max-w-5xl mx-auto px-6 py-3 flex items-center gap-4">
|
|
142
|
+
<div class="flex items-center gap-2">
|
|
143
|
+
<div class="w-8 h-8 rounded-lg bg-cyan-500/10 border border-cyan-500/30 flex items-center justify-center">
|
|
144
|
+
<span class="text-cyan-400 text-lg font-bold">D</span>
|
|
145
|
+
</div>
|
|
146
|
+
<span class="font-semibold text-slate-100">DCC</span>
|
|
147
|
+
</div>
|
|
148
|
+
<!-- Tabs -->
|
|
149
|
+
<div class="flex gap-1 ml-2">
|
|
150
|
+
<button @click="mode = 'validate'" :class="['px-3 py-1.5 text-xs font-medium rounded-lg transition-colors', mode === 'validate' ? 'bg-cyan-500/10 text-cyan-400 border border-cyan-500/30' : 'text-slate-500 hover:text-slate-300']">Validate</button>
|
|
151
|
+
<button @click="mode = 'create'" :class="['px-3 py-1.5 text-xs font-medium rounded-lg transition-colors', mode === 'create' ? 'bg-cyan-500/10 text-cyan-400 border border-cyan-500/30' : 'text-slate-500 hover:text-slate-300']">Create</button>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="flex-1"></div>
|
|
154
|
+
<template v-if="mode === 'validate'">
|
|
155
|
+
<button @click="loadDemo" class="px-3 py-1.5 text-xs font-medium rounded-lg border border-slate-700 hover:border-cyan-500/50 hover:text-cyan-400 transition-colors">Demo</button>
|
|
156
|
+
<label class="cursor-pointer px-3 py-1.5 text-xs font-medium rounded-lg bg-cyan-500/10 border border-cyan-500/30 text-cyan-400 hover:bg-cyan-500/20 transition-colors">Upload XML<input type="file" accept=".xml,.dcc" class="hidden" @change="onUpload" /></label>
|
|
157
|
+
</template>
|
|
158
|
+
<template v-else>
|
|
159
|
+
<button @click="loadDemoForm" class="px-3 py-1.5 text-xs font-medium rounded-lg border border-slate-700 hover:border-cyan-500/50 hover:text-cyan-400 transition-colors">Fill Demo</button>
|
|
160
|
+
<button @click="generatePreview" class="px-3 py-1.5 text-xs font-medium rounded-lg bg-cyan-500/10 border border-cyan-500/30 text-cyan-400 hover:bg-cyan-500/20 transition-colors">Generate</button>
|
|
161
|
+
</template>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<!-- ===== VALIDATE ===== -->
|
|
166
|
+
<template v-if="mode === 'validate'">
|
|
167
|
+
<div v-if="!cert && !loading" class="max-w-2xl mx-auto px-6 pt-24">
|
|
168
|
+
<div @dragover.prevent="dragActive = true" @dragleave.prevent="dragActive = false" @drop.prevent="onDrop" :class="['border-2 border-dashed rounded-2xl p-16 text-center transition-all', dragActive ? 'border-cyan-500 bg-cyan-500/5' : 'border-slate-800']">
|
|
169
|
+
<div class="text-5xl mb-4 opacity-30">📋</div>
|
|
170
|
+
<p class="text-slate-400 mb-1">Drag a DCC XML file here</p>
|
|
171
|
+
<p class="text-slate-600 text-sm">or click upload above</p>
|
|
172
|
+
<button @click="loadDemo" class="mt-6 px-4 py-2 text-sm font-medium rounded-lg bg-cyan-500/10 border border-cyan-500/30 text-cyan-400 hover:bg-cyan-500/20 transition-colors">Try a demo certificate →</button>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
<div v-if="loading" class="max-w-2xl mx-auto px-6 pt-32 text-center">
|
|
176
|
+
<div class="inline-block w-8 h-8 border-2 border-cyan-500/30 border-t-cyan-500 rounded-full animate-spin mb-4"></div>
|
|
177
|
+
<p class="text-slate-400 text-sm">Validating...</p>
|
|
178
|
+
</div>
|
|
179
|
+
<div v-if="cert && !loading" class="max-w-5xl mx-auto px-6 py-8 space-y-4">
|
|
180
|
+
<div :class="['rounded-2xl p-5 border slide-up flex items-center gap-4', validation.ok ? 'bg-green-500/5 border-green-500/20' : 'bg-red-500/5 border-red-500/20']">
|
|
181
|
+
<div :class="['w-12 h-12 rounded-xl flex items-center justify-center text-2xl', validation.ok ? 'bg-green-500/10' : 'bg-red-500/10']">{{ validation.ok ? '✓' : '✕' }}</div>
|
|
182
|
+
<div class="flex-1">
|
|
183
|
+
<div :class="['font-semibold text-lg', validation.ok ? 'text-green-400' : 'text-red-400']">{{ validation.ok ? 'Certificate Valid' : 'Validation Failed' }}</div>
|
|
184
|
+
<div class="text-sm text-slate-500">{{ errors.length }} error{{ errors.length === 1 ? '' : 's' }}, {{ warnings.length }} warning{{ warnings.length === 1 ? '' : 's' }}<span v-if="cert.schemaVersion"> · Schema v{{ cert.schemaVersion }}</span></div>
|
|
185
|
+
</div>
|
|
186
|
+
<div :class="['flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium', validation.ok ? 'bg-green-500/10 text-green-400 border border-green-500/20' : 'bg-red-500/10 text-red-400 border border-red-500/20']"><span class="w-1.5 h-1.5 rounded-full pulse-dot" :class="validation.ok ? 'bg-green-400' : 'bg-red-400'"></span>{{ validation.ok ? 'VALID' : 'INVALID' }}</div>
|
|
187
|
+
</div>
|
|
188
|
+
<div v-if="errors.length" class="slide-up rounded-xl border border-red-500/15 bg-red-500/[0.03] overflow-hidden">
|
|
189
|
+
<div class="px-5 py-3 border-b border-red-500/10 text-xs font-semibold uppercase tracking-wide text-red-400/80">Validation Errors</div>
|
|
190
|
+
<div class="divide-y divide-red-500/5"><div v-for="(e, i) in errors" :key="i" class="px-5 py-3 flex gap-3"><span class="text-red-500/50 text-xs font-mono pt-0.5">{{ e.line ? 'L' + e.line : '—' }}</span><span class="text-sm text-slate-300">{{ e.message }}</span></div></div>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="slide-up rounded-2xl border border-slate-800 bg-slate-900 overflow-hidden">
|
|
193
|
+
<div class="bg-gradient-to-br from-slate-900 to-slate-800/50 p-6 relative overflow-hidden">
|
|
194
|
+
<div class="absolute top-0 right-0 w-64 h-64 bg-cyan-500/5 rounded-full blur-3xl"></div>
|
|
195
|
+
<div class="relative flex justify-between items-start gap-4">
|
|
196
|
+
<div>
|
|
197
|
+
<div class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-semibold uppercase tracking-wider border border-cyan-500/30 text-cyan-400 bg-cyan-500/5 mb-3"><span class="w-1 h-1 rounded-full bg-cyan-400 pulse-dot"></span>Digital Calibration Certificate</div>
|
|
198
|
+
<h1 class="text-2xl font-bold text-slate-100 mb-1">{{ cert.itemName || 'Calibration Certificate' }}</h1>
|
|
199
|
+
<p class="text-slate-400">{{ cert.labName }}</p>
|
|
200
|
+
<p class="font-mono text-xs text-slate-600 mt-1.5">ID: {{ cert.uniqueId || '—' }}</p>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="text-right border-l border-slate-700 pl-6">
|
|
203
|
+
<div class="text-[10px] uppercase tracking-wider text-slate-500">Schema</div><div class="font-mono text-green-400 font-medium">v{{ cert.schemaVersion }}</div>
|
|
204
|
+
<div v-if="cert.country" class="mt-2"><div class="text-[10px] uppercase tracking-wider text-slate-500">Country</div><div class="font-mono text-lg">{{ cert.country }}</div></div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
<div v-if="cert.coreData.length" class="slide-up rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
|
|
210
|
+
<div class="px-5 py-3 border-b border-slate-800 text-xs font-semibold uppercase tracking-wide text-slate-500">Core Data</div>
|
|
211
|
+
<div class="grid grid-cols-2 sm:grid-cols-3 gap-px bg-slate-800/50"><div v-for="f in cert.coreData" :key="f.label" class="bg-slate-900 p-3"><div class="text-[10px] uppercase tracking-wider text-slate-600 mb-0.5">{{ f.label }}</div><div class="font-mono text-sm text-slate-200">{{ f.value }}</div></div></div>
|
|
212
|
+
</div>
|
|
213
|
+
<div v-if="cert.measurements.length" class="slide-up rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
|
|
214
|
+
<div class="px-5 py-3 border-b border-slate-800 text-xs font-semibold uppercase tracking-wide text-slate-500">Measurement Results</div>
|
|
215
|
+
<div class="p-5 space-y-3">
|
|
216
|
+
<div v-for="mr in cert.measurements" :key="mr.name" class="rounded-lg border border-slate-800 bg-slate-800/40 p-4">
|
|
217
|
+
<div class="font-medium text-slate-200 mb-3">{{ mr.name }}</div>
|
|
218
|
+
<div class="space-y-2">
|
|
219
|
+
<div v-for="r in mr.results" :key="r.name" class="py-2 border-l-2 border-cyan-500/30 pl-3 bg-cyan-500/[0.03] rounded-r-lg">
|
|
220
|
+
<div class="text-sm text-slate-400 mb-1">{{ r.name }}</div>
|
|
221
|
+
<div v-for="q in r.quantities" :key="q.name" class="flex items-baseline gap-2 font-mono text-sm pl-2"><span v-if="q.name !== r.name" class="text-slate-500 text-xs">{{ q.name }}:</span><span class="text-green-400">{{ q.value }}</span><span class="text-slate-600">{{ q.unit }}</span><span v-if="q.uncertainty" class="text-amber-400/60 text-xs">±{{ q.uncertainty }}</span></div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
<div v-if="cert.signature?.present" class="slide-up rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
|
|
228
|
+
<div class="px-5 py-3 border-b border-slate-800 flex items-center gap-2"><span>🔏</span><span class="text-xs font-semibold uppercase tracking-wide text-slate-500">Digital Signature</span></div>
|
|
229
|
+
<div class="p-5">
|
|
230
|
+
<div v-if="sigVerifying" class="flex items-center gap-3"><div class="inline-block w-5 h-5 border-2 border-cyan-500/30 border-t-cyan-500 rounded-full animate-spin"></div><span class="text-sm text-slate-400">Verifying signature...</span></div>
|
|
231
|
+
<div v-else-if="sigResult?.verified" class="flex items-center gap-4">
|
|
232
|
+
<div :class="['w-10 h-10 rounded-lg flex items-center justify-center text-xl', sigResult.result ? 'bg-green-500/10 text-green-400' : 'bg-red-500/10 text-red-400']">{{ sigResult.result ? '✓' : '✕' }}</div>
|
|
233
|
+
<div class="flex-1"><div :class="['font-medium', sigResult.result ? 'text-green-400' : 'text-red-400']">{{ sigResult.result ? 'Signature Valid' : 'Signature Invalid' }}</div><div v-if="sigResult.error" class="text-xs text-red-400/60 mt-0.5">{{ sigResult.error }}</div></div>
|
|
234
|
+
</div>
|
|
235
|
+
<div v-else class="flex items-center gap-4"><div class="w-10 h-10 rounded-lg bg-amber-500/10 flex items-center justify-center text-xl text-amber-400">⚠</div><div class="font-medium text-amber-400">Signature Present — NOT Verified</div></div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
<div class="slide-up grid sm:grid-cols-2 gap-4">
|
|
239
|
+
<div v-if="cert.labName" class="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden"><div class="px-5 py-3 border-b border-slate-800 text-xs font-semibold uppercase tracking-wide text-slate-500">Calibration Laboratory</div><div class="p-5"><div class="font-medium text-slate-200 mb-1">{{ cert.labName }}</div><div v-if="cert.labEmail" class="font-mono text-xs text-slate-500">{{ cert.labEmail }}</div></div></div>
|
|
240
|
+
<div v-if="cert.signers.length" class="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden"><div class="px-5 py-3 border-b border-slate-800 text-xs font-semibold uppercase tracking-wide text-slate-500">Responsible Persons</div><div class="p-5 space-y-2"><div v-for="s in cert.signers" :key="s.name" class="flex items-center gap-3 rounded-lg border border-slate-800 bg-slate-800/40 px-3 py-2"><div class="w-8 h-8 rounded-full bg-cyan-500 text-slate-900 flex items-center justify-center text-xs font-bold">{{ initials(s.name) }}</div><div class="flex-1"><div class="text-sm font-medium text-slate-200">{{ s.name }}</div><div v-if="s.email" class="font-mono text-xs text-slate-500">{{ s.email }}</div></div><span v-if="s.mainSigner" class="px-2 py-0.5 rounded-full text-[10px] font-semibold uppercase tracking-wider text-green-400 border border-green-500/20 bg-green-500/10">Main</span></div></div></div>
|
|
241
|
+
</div>
|
|
242
|
+
<div class="slide-up rounded-xl border border-slate-800 bg-slate-900 overflow-hidden"><button @click="showXml = !showXml" class="w-full px-5 py-3 flex items-center justify-between text-xs font-semibold uppercase tracking-wide text-slate-500 hover:bg-slate-800/50 transition-colors"><span>XML Source</span><span>{{ showXml ? '▲' : '▼' }}</span></button><div v-if="showXml" class="p-5 overflow-auto max-h-96"><pre class="text-xs text-slate-400 font-mono whitespace-pre-wrap">{{ rawXml }}</pre></div></div>
|
|
243
|
+
</div>
|
|
244
|
+
</template>
|
|
245
|
+
|
|
246
|
+
<!-- ===== CREATE ===== -->
|
|
247
|
+
<template v-else>
|
|
248
|
+
<div class="max-w-3xl mx-auto px-6 py-8 space-y-5">
|
|
249
|
+
<div class="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden slide-up">
|
|
250
|
+
<div class="px-5 py-3 border-b border-slate-800 text-xs font-semibold uppercase tracking-wide text-slate-500">Certificate Identity</div>
|
|
251
|
+
<div class="p-5 grid grid-cols-2 gap-4">
|
|
252
|
+
<label class="block"><span class="text-xs text-slate-500">Unique Identifier *</span><input v-model="form.uniqueId" placeholder="e.g. PTB-12345-2026" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-cyan-500 focus:outline-none" /></label>
|
|
253
|
+
<label class="block"><span class="text-xs text-slate-500">Country (ISO 3166-1) *</span><input v-model="form.country" maxlength="2" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 uppercase focus:border-cyan-500 focus:outline-none" /></label>
|
|
254
|
+
<label class="block"><span class="text-xs text-slate-500">Languages</span><input v-model="form.languages" placeholder="en, de" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-cyan-500 focus:outline-none" /></label>
|
|
255
|
+
<label class="block"><span class="text-xs text-slate-500">Schema Version</span><select v-model="form.schemaVersion" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-cyan-500 focus:outline-none"><option>3.3.0</option><option>3.2.1</option><option>2.3.0</option></select></label>
|
|
256
|
+
<label class="block"><span class="text-xs text-slate-500">Begin Date</span><input type="date" v-model="form.beginDate" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-cyan-500 focus:outline-none" /></label>
|
|
257
|
+
<label class="block"><span class="text-xs text-slate-500">End Date</span><input type="date" v-model="form.endDate" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-cyan-500 focus:outline-none" /></label>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
<div class="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden slide-up">
|
|
261
|
+
<div class="px-5 py-3 border-b border-slate-800 text-xs font-semibold uppercase tracking-wide text-slate-500">Calibrated Item</div>
|
|
262
|
+
<div class="p-5 grid grid-cols-2 gap-4">
|
|
263
|
+
<label class="block col-span-2"><span class="text-xs text-slate-500">Item Name *</span><input v-model="form.itemName" placeholder="e.g. Resistance Thermometer Pt100" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-cyan-500 focus:outline-none" /></label>
|
|
264
|
+
<label class="block"><span class="text-xs text-slate-500">Model</span><input v-model="form.itemModel" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-cyan-500 focus:outline-none" /></label>
|
|
265
|
+
<label class="block"><span class="text-xs text-slate-500">Manufacturer</span><input v-model="form.manufacturer" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-cyan-500 focus:outline-none" /></label>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden slide-up">
|
|
269
|
+
<div class="px-5 py-3 border-b border-slate-800 text-xs font-semibold uppercase tracking-wide text-slate-500">Calibration Laboratory</div>
|
|
270
|
+
<div class="p-5 grid grid-cols-2 gap-4">
|
|
271
|
+
<label class="block"><span class="text-xs text-slate-500">Lab Name</span><input v-model="form.labName" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-cyan-500 focus:outline-none" /></label>
|
|
272
|
+
<label class="block"><span class="text-xs text-slate-500">Lab Code</span><input v-model="form.labCode" placeholder="e.g. DE1, NL1" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-cyan-500 focus:outline-none" /></label>
|
|
273
|
+
<label class="block col-span-2"><span class="text-xs text-slate-500">Email</span><input v-model="form.labEmail" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-cyan-500 focus:outline-none" /></label>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
<div class="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden slide-up">
|
|
277
|
+
<div class="px-5 py-3 border-b border-slate-800 text-xs font-semibold uppercase tracking-wide text-slate-500">Responsible Person</div>
|
|
278
|
+
<div class="p-5 grid grid-cols-2 gap-4">
|
|
279
|
+
<label class="block"><span class="text-xs text-slate-500">Name</span><input v-model="form.signerName" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-cyan-500 focus:outline-none" /></label>
|
|
280
|
+
<label class="block"><span class="text-xs text-slate-500">Email</span><input v-model="form.signerEmail" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-cyan-500 focus:outline-none" /></label>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
<div class="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden slide-up">
|
|
284
|
+
<div class="px-5 py-3 border-b border-slate-800 text-xs font-semibold uppercase tracking-wide text-slate-500">Customer</div>
|
|
285
|
+
<div class="p-5"><label class="block"><span class="text-xs text-slate-500">Customer Name</span><input v-model="form.customerName" class="mt-1 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-cyan-500 focus:outline-none" /></label></div>
|
|
286
|
+
</div>
|
|
287
|
+
<div class="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden slide-up">
|
|
288
|
+
<div class="px-5 py-3 border-b border-slate-800 flex items-center justify-between">
|
|
289
|
+
<span class="text-xs font-semibold uppercase tracking-wide text-slate-500">Measurement Results</span>
|
|
290
|
+
<button @click="addMeasurement" class="text-xs text-cyan-400 hover:text-cyan-300">+ Add Result</button>
|
|
291
|
+
</div>
|
|
292
|
+
<div class="p-5 space-y-4">
|
|
293
|
+
<div v-for="(m, mi) in form.measurements" :key="mi" class="rounded-lg border border-slate-800 bg-slate-800/40 p-4">
|
|
294
|
+
<div class="flex items-center gap-2 mb-3">
|
|
295
|
+
<input v-model="m.name" :placeholder="'Result ' + (mi + 1)" class="flex-1 bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-cyan-500 focus:outline-none" />
|
|
296
|
+
<button v-if="form.measurements.length > 1" @click="removeMeasurement(mi)" class="text-xs text-red-400/60 hover:text-red-400">Remove</button>
|
|
297
|
+
</div>
|
|
298
|
+
<div class="space-y-3">
|
|
299
|
+
<div v-for="(q, qi) in m.quantities" :key="qi" class="rounded-lg border border-slate-700/50 bg-slate-900/60 p-3 space-y-2">
|
|
300
|
+
<!-- Row 1: name + value -->
|
|
301
|
+
<div class="grid grid-cols-3 gap-2">
|
|
302
|
+
<input v-model="q.name" placeholder="Quantity name" class="bg-slate-800 border border-slate-700 rounded-lg px-2.5 py-1.5 text-xs text-slate-200 focus:border-cyan-500 focus:outline-none" />
|
|
303
|
+
<input v-model="q.value" placeholder="Measured value" class="bg-slate-800 border border-slate-700 rounded-lg px-2.5 py-1.5 text-xs text-green-400/90 font-mono focus:border-cyan-500 focus:outline-none" />
|
|
304
|
+
<button v-if="m.quantities.length > 1" @click="removeQuantity(mi, qi)" class="text-xs text-red-400/40 hover:text-red-400 justify-self-end px-2">Remove</button>
|
|
305
|
+
</div>
|
|
306
|
+
<!-- Row 2: quantity type + unit picker -->
|
|
307
|
+
<div class="grid grid-cols-2 gap-2">
|
|
308
|
+
<select v-model="q.quantityType" @change="onQuantityTypeChange(q)" class="bg-slate-800 border border-slate-700 rounded-lg px-2.5 py-1.5 text-xs text-slate-300 focus:border-cyan-500 focus:outline-none">
|
|
309
|
+
<option value="">Select quantity type...</option>
|
|
310
|
+
<option v-for="qt in quantities" :key="qt.short" :value="qt.short">{{ qt.name }}</option>
|
|
311
|
+
</select>
|
|
312
|
+
<select v-model="q.unitLabel" @change="onUnitChange(q)" :disabled="!q.quantityType" class="bg-slate-800 border border-slate-700 rounded-lg px-2.5 py-1.5 text-xs text-slate-400 font-mono focus:border-cyan-500 focus:outline-none disabled:opacity-40">
|
|
313
|
+
<option value="">Unit...</option>
|
|
314
|
+
<option v-for="u in (quantities.find(qt => qt.short === q.quantityType)?.units || [])" :key="u.short" :value="u.short">{{ u.symbol }} ({{ u.short }})</option>
|
|
315
|
+
</select>
|
|
316
|
+
</div>
|
|
317
|
+
<!-- Row 3: uncertainty (structured) -->
|
|
318
|
+
<div class="flex items-center gap-2 pt-1">
|
|
319
|
+
<label class="flex items-center gap-1.5 text-xs text-slate-500 cursor-pointer">
|
|
320
|
+
<input type="checkbox" v-model="q.hasUncertainty" class="accent-cyan-500" />
|
|
321
|
+
Expanded uncertainty
|
|
322
|
+
</label>
|
|
323
|
+
<template v-if="q.hasUncertainty">
|
|
324
|
+
<input v-model="q.uncertainty" placeholder="U" class="w-20 bg-slate-800 border border-slate-700 rounded-lg px-2 py-1 text-xs text-amber-400/70 font-mono focus:border-cyan-500 focus:outline-none" />
|
|
325
|
+
<span class="text-slate-600 text-xs">k=</span>
|
|
326
|
+
<select v-model="q.coverageFactor" @change="onCoverageFactorChange(q)" class="bg-slate-800 border border-slate-700 rounded-lg px-2 py-1 text-xs text-slate-400 font-mono focus:border-cyan-500 focus:outline-none">
|
|
327
|
+
<option value="1">1</option><option value="2">2</option><option value="3">3</option>
|
|
328
|
+
</select>
|
|
329
|
+
<span class="text-slate-600 text-xs">p={{ q.coverageProbability }}</span>
|
|
330
|
+
</template>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
<button @click="addQuantity(mi)" class="mt-2 text-xs text-cyan-400/70 hover:text-cyan-400">+ Add Quantity</button>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
<div v-if="showPreview" class="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden slide-up">
|
|
339
|
+
<div class="px-5 py-3 border-b border-slate-800 flex items-center justify-between">
|
|
340
|
+
<span class="text-xs font-semibold uppercase tracking-wide" :class="buildErrors.length ? 'text-red-400' : 'text-green-400'">{{ buildErrors.length ? 'Validation Issues' : 'DCC XML Generated ✓' }}</span>
|
|
341
|
+
<button @click="downloadXml" class="px-3 py-1.5 text-xs font-medium rounded-lg bg-cyan-500/10 border border-cyan-500/30 text-cyan-400 hover:bg-cyan-500/20 transition-colors">⬇ Download .xml</button>
|
|
342
|
+
</div>
|
|
343
|
+
<div v-if="buildErrors.length" class="px-5 py-3 space-y-1"><div v-for="(e, i) in buildErrors" :key="i" class="text-xs text-red-400/80">• {{ e }}</div></div>
|
|
344
|
+
<div class="p-5 overflow-auto max-h-64"><pre class="text-xs text-slate-400 font-mono whitespace-pre-wrap">{{ previewXml }}</pre></div>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
</template>
|
|
348
|
+
</div>
|
|
349
|
+
</template>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
export interface BuilderQuantity {
|
|
2
|
+
name: string;
|
|
3
|
+
value: string;
|
|
4
|
+
quantityType: string;
|
|
5
|
+
unitSiunitx: string;
|
|
6
|
+
unitLabel: string;
|
|
7
|
+
hasUncertainty: boolean;
|
|
8
|
+
uncertainty: string;
|
|
9
|
+
coverageFactor: string;
|
|
10
|
+
coverageProbability: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BuilderForm {
|
|
14
|
+
schemaVersion: string;
|
|
15
|
+
uniqueId: string;
|
|
16
|
+
country: string;
|
|
17
|
+
languages: string;
|
|
18
|
+
beginDate: string;
|
|
19
|
+
endDate: string;
|
|
20
|
+
itemName: string;
|
|
21
|
+
itemModel: string;
|
|
22
|
+
manufacturer: string;
|
|
23
|
+
labName: string;
|
|
24
|
+
labEmail: string;
|
|
25
|
+
labCode: string;
|
|
26
|
+
signerName: string;
|
|
27
|
+
signerEmail: string;
|
|
28
|
+
customerName: string;
|
|
29
|
+
measurements: { name: string; quantities: BuilderQuantity[] }[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function emptyForm(): BuilderForm {
|
|
33
|
+
return {
|
|
34
|
+
schemaVersion: "3.3.0",
|
|
35
|
+
uniqueId: "",
|
|
36
|
+
country: "DE",
|
|
37
|
+
languages: "en",
|
|
38
|
+
beginDate: new Date().toISOString().split("T")[0],
|
|
39
|
+
endDate: new Date().toISOString().split("T")[0],
|
|
40
|
+
itemName: "",
|
|
41
|
+
itemModel: "",
|
|
42
|
+
manufacturer: "",
|
|
43
|
+
labName: "",
|
|
44
|
+
labEmail: "",
|
|
45
|
+
labCode: "",
|
|
46
|
+
signerName: "",
|
|
47
|
+
signerEmail: "",
|
|
48
|
+
customerName: "",
|
|
49
|
+
measurements: [
|
|
50
|
+
{
|
|
51
|
+
name: "",
|
|
52
|
+
quantities: [emptyQuantity()],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function emptyQuantity(): BuilderQuantity {
|
|
59
|
+
return {
|
|
60
|
+
name: "", value: "", quantityType: "", unitSiunitx: "", unitLabel: "",
|
|
61
|
+
hasUncertainty: true, uncertainty: "", coverageFactor: "2", coverageProbability: "0.95",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function esc(s: string): string {
|
|
66
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function generateDccXml(form: BuilderForm): string {
|
|
70
|
+
const langs = form.languages.split(",").map((l) => l.trim()).filter(Boolean);
|
|
71
|
+
const langTags = langs.map((l) => `<dcc:usedLangCodeISO639_1>${esc(l)}</dcc:usedLangCodeISO639_1>`).join("\n ");
|
|
72
|
+
const mandatoryLang = langs[0] || "en";
|
|
73
|
+
|
|
74
|
+
const quantityXml = form.measurements
|
|
75
|
+
.filter((m) => m.name || m.quantities.some((q) => q.value))
|
|
76
|
+
.map((m) => {
|
|
77
|
+
const quants = m.quantities
|
|
78
|
+
.filter((q) => q.value)
|
|
79
|
+
.map((q) => {
|
|
80
|
+
const unc = (q.hasUncertainty && q.uncertainty)
|
|
81
|
+
? `\n <si:expandedMU>
|
|
82
|
+
<si:valueExpandedMU>${esc(q.uncertainty)}</si:valueExpandedMU>
|
|
83
|
+
<si:coverageFactor>${esc(q.coverageFactor || "2")}</si:coverageFactor>
|
|
84
|
+
<si:coverageProbability>${esc(q.coverageProbability || "0.95")}</si:coverageProbability>
|
|
85
|
+
</si:expandedMU>`
|
|
86
|
+
: "";
|
|
87
|
+
return ` <dcc:quantity>
|
|
88
|
+
<dcc:name><dcc:content lang="en">${esc(q.name || q.quantityType || "Value")}</dcc:content></dcc:name>
|
|
89
|
+
<si:real>
|
|
90
|
+
<si:value>${esc(q.value)}</si:value>${q.unitSiunitx ? `\n <si:unit>${esc(q.unitSiunitx)}</si:unit>` : ""}${unc}
|
|
91
|
+
</si:real>
|
|
92
|
+
</dcc:quantity>`;
|
|
93
|
+
})
|
|
94
|
+
.join("\n");
|
|
95
|
+
|
|
96
|
+
return ` <dcc:result>
|
|
97
|
+
<dcc:name><dcc:content lang="en">${esc(m.name)}</dcc:content></dcc:name>
|
|
98
|
+
<dcc:data>
|
|
99
|
+
${quants}
|
|
100
|
+
</dcc:data>
|
|
101
|
+
</dcc:result>`;
|
|
102
|
+
})
|
|
103
|
+
.join("\n");
|
|
104
|
+
|
|
105
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
106
|
+
<dcc:digitalCalibrationCertificate
|
|
107
|
+
xmlns:dcc="https://ptb.de/dcc"
|
|
108
|
+
xmlns:si="https://ptb.de/si"
|
|
109
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
110
|
+
xsi:schemaLocation="https://ptb.de/dcc https://ptb.de/dcc/v${esc(form.schemaVersion)}/dcc.xsd"
|
|
111
|
+
schemaVersion="${esc(form.schemaVersion)}">
|
|
112
|
+
|
|
113
|
+
<dcc:administrativeData>
|
|
114
|
+
<dcc:dccSoftware>
|
|
115
|
+
<dcc:software>
|
|
116
|
+
<dcc:name><dcc:content lang="en">DCC Validator Builder</dcc:content></dcc:name>
|
|
117
|
+
<dcc:release>0.1.0</dcc:release>
|
|
118
|
+
</dcc:software>
|
|
119
|
+
</dcc:dccSoftware>
|
|
120
|
+
|
|
121
|
+
<dcc:coreData>
|
|
122
|
+
<dcc:countryCodeISO3166_1>${esc(form.country)}</dcc:countryCodeISO3166_1>
|
|
123
|
+
${langTags}
|
|
124
|
+
<dcc:mandatoryLangCodeISO639_1>${esc(mandatoryLang)}</dcc:mandatoryLangCodeISO639_1>
|
|
125
|
+
<dcc:uniqueIdentifier>${esc(form.uniqueId)}</dcc:uniqueIdentifier>
|
|
126
|
+
<dcc:beginPerformanceDate>${esc(form.beginDate)}</dcc:beginPerformanceDate>
|
|
127
|
+
<dcc:endPerformanceDate>${esc(form.endDate)}</dcc:endPerformanceDate>
|
|
128
|
+
</dcc:coreData>
|
|
129
|
+
|
|
130
|
+
<dcc:items>
|
|
131
|
+
<dcc:item>
|
|
132
|
+
<dcc:name><dcc:content lang="en">${esc(form.itemName)}</dcc:content></dcc:name>
|
|
133
|
+
<dcc:manufacturer><dcc:name><dcc:content lang="en">${esc(form.manufacturer)}</dcc:content></dcc:name></dcc:manufacturer>
|
|
134
|
+
<dcc:model>${esc(form.itemModel)}</dcc:model>
|
|
135
|
+
</dcc:item>
|
|
136
|
+
</dcc:items>
|
|
137
|
+
|
|
138
|
+
<dcc:calibrationLaboratory>
|
|
139
|
+
<dcc:calibrationLaboratoryCode>${esc(form.labCode)}</dcc:calibrationLaboratoryCode>
|
|
140
|
+
<dcc:contact>
|
|
141
|
+
<dcc:name><dcc:content lang="en">${esc(form.labName)}</dcc:content></dcc:name>
|
|
142
|
+
<dcc:eMail>${esc(form.labEmail)}</dcc:eMail>
|
|
143
|
+
</dcc:contact>
|
|
144
|
+
</dcc:calibrationLaboratory>
|
|
145
|
+
|
|
146
|
+
<dcc:respPersons>
|
|
147
|
+
<dcc:respPerson>
|
|
148
|
+
<dcc:person>
|
|
149
|
+
<dcc:name><dcc:content lang="en">${esc(form.signerName)}</dcc:content></dcc:name>
|
|
150
|
+
<dcc:eMail>${esc(form.signerEmail)}</dcc:eMail>
|
|
151
|
+
</dcc:person>
|
|
152
|
+
<dcc:mainSigner>true</dcc:mainSigner>
|
|
153
|
+
</dcc:respPerson>
|
|
154
|
+
</dcc:respPersons>
|
|
155
|
+
|
|
156
|
+
<dcc:customer>
|
|
157
|
+
<dcc:name><dcc:content lang="en">${esc(form.customerName)}</dcc:content></dcc:name>
|
|
158
|
+
</dcc:customer>
|
|
159
|
+
</dcc:administrativeData>
|
|
160
|
+
|
|
161
|
+
<dcc:measurementResults>
|
|
162
|
+
<dcc:measurementResult>
|
|
163
|
+
<dcc:name><dcc:content lang="en">Calibration Results</dcc:content></dcc:name>
|
|
164
|
+
<dcc:results>
|
|
165
|
+
${quantityXml}
|
|
166
|
+
</dcc:results>
|
|
167
|
+
</dcc:measurementResult>
|
|
168
|
+
</dcc:measurementResults>
|
|
169
|
+
</dcc:digitalCalibrationCertificate>
|
|
170
|
+
`;
|
|
171
|
+
}
|