Skip to content

Commit

Permalink
import articles
Browse files Browse the repository at this point in the history
  • Loading branch information
Kworz committed Mar 19, 2024
1 parent 837c430 commit 3341b7a
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 37 deletions.
3 changes: 2 additions & 1 deletion src/lib/i18n/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@
"store_location": "Store location",
"store_name": "Store name",
"store": "Store",
"store_list_linked": "List linked store"
"store_list_linked": "List linked store",
"warning": "Warning"
},
"time": {
"hours": "Hours"
Expand Down
9 changes: 7 additions & 2 deletions src/lib/i18n/lang/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@
"false": "Non"
},
"ok": "Ok",
"required_quantity": "Quantité nécéssaire"
"required_quantity": "Quantité nécéssaire",
"warning": "Attention",
"order_quantity": "Quantité de commande minimum",
"unit_of_work_quantity": "Quantité d'unité d'oeuvre",
"non_physical": "Article non physique"
},
"time": {
"hours": "Heures"
Expand Down Expand Up @@ -377,5 +381,6 @@
},
"generic": "Une erreur est survenue. Consulter la console serveur pour avoir plus de détails.",
"unauthed": "Action interdite, vous devez vous authentifier."
}
},
"internal": "Article interne"
}
77 changes: 64 additions & 13 deletions src/routes/app/(scm)/scm/articles/import/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,95 @@
import { unit_of_work } from "$lib/prisma-enums";
import type { scm_article } from "@prisma/client";
import { redirect, type Actions } from "@sveltejs/kit";
import { fail, redirect, type Actions } from "@sveltejs/kit";

export const actions: Actions = {
import: async ({ request, locals }) => {

const warnings = [];

try
{
// TODO: Check this
const form = await request.formData();
const file = form.get("file");
const columnOrder = form.get("columnOrder");
const columnOrder = form.get("column_order");

const colSplitter = form.get("col_splitter")?.toString();
const rowSplitter = form.get("row_splitter")?.toString();

if(!(file instanceof File))
throw "article.import.file.missing";
return fail(400, { import: { error: "errors.article.import.missing_file" }});

if(columnOrder === null)
throw "article.import.columnOrder.missing";
return fail(500, { import: { error: "errors.article.import.column_order_invalid" }});

if(colSplitter === undefined || !([",", ";"].includes(colSplitter)))
return fail(400, { import: { error : "errors.article.import.col_splitter_invalid" }});

if(rowSplitter === undefined || !(["n", "rn"].includes(rowSplitter)))
return fail(400, { import: { error: "errors.article.import.row_splitter_invalid" }});

const fileContent = await file.text();
const columns = JSON.parse(columnOrder.toString()) as (string | null)[];

const lines = fileContent.split("\r\n").map(line => line.split(","));
console.log(columns);

for(const line of lines)
const lineSplitter = rowSplitter === "rn" ? "\r\n" : "\n";

const lines = fileContent.split(lineSplitter).map(line => line.split(colSplitter));

for(const line of lines.slice(1))
{
const article = {

const nonPhysicalValue = line[columns.indexOf("non_physical")];
const consumableValue = line[columns.indexOf("consumable")];
const internalValue = line[columns.indexOf("internal")];

const article: Partial<scm_article> = {
name: line[columns.indexOf("name")],
reference: line[columns.indexOf("reference")],
brand: line[columns.indexOf("manufacturer")],

} satisfies Partial<scm_article>;
reference: line[columns.indexOf("sku")],
brand: line[columns.indexOf("brand")],

order_quantity: Number(line[columns.indexOf("order_quantity")]) || undefined,
critical_quantity: Number(line[columns.indexOf("critical_quantity")]) || undefined,

non_physical: nonPhysicalValue === "true" || nonPhysicalValue === "1",
consumable: consumableValue === "true" || consumableValue === "1",
internal: internalValue === "true" || internalValue === "1",
};

// Parse article unit
const articleUnit = line[columns.indexOf("unit_of_work")];
if(articleUnit !== "")
{
if(Object.keys(unit_of_work).includes(articleUnit))
{
article.unit = articleUnit as unit_of_work;
article.unit_quantity = Number(line[columns.indexOf("unit_of_work_quantity")]) || null;
}
else
{
warnings.push(`Found unit ${articleUnit} which is not compatible with ${Object.keys(unit_of_work).join(",")} for article ${article.reference}`);
}
}

if(article.brand === undefined || article.name === undefined || article.reference === undefined) // skip rows with missing data
continue;

console.log("dry-run", article);

await locals.prisma.scm_article.create({ data: article });
}

throw redirect(303, "/app/scm/articles");

if(warnings.length > 0)
return { import: { success: true, warnings }};
}
catch(ex)
{
console.log(ex);
return { error: true }
}

return redirect(303, "/app/scm/articles");
}
}
85 changes: 64 additions & 21 deletions src/routes/app/(scm)/scm/articles/import/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,77 +4,120 @@
import Flex from "$lib/components/generics/layout/flex.svelte";
import Table from "$lib/components/generics/table/Table.svelte";
import TableCell from "$lib/components/generics/table/TableCell.svelte";
import { _ } from "svelte-i18n";
import type { ActionData } from "./$types";
import Modal from "$lib/components/generics/modal/Modal.svelte";
export let form: ActionData;
let file: File | undefined = undefined;
let columns: (string | null)[] = [];
const availableColumns = ["Référence", "Désignation", "Fabricant"];
let splitter: "," | ";" = ",";
let rowSplitter: "n" | "rn" = "n";
let importSuspense = false;
const parseCSV = async (file: File): Promise<{ headers: string[], lines: string[][]}> => {
const availableColumns = ["sku", "name", "brand", "non_physical", "consumable", "internal", "unit_of_work", "unit_of_work_quantity", "order_quantity", "critical_quantity"];
const parseCSV = async (file: File, splitter: "," | ";" = ",", lineSplitter: "n" | "rn" = "n"): Promise<{ headers: string[], lines: string[][]}> => {
const text = await file.text();
const lines = text.split("\r\n");
const headers = lines[0].split(",");
const lineSplitterRegex = lineSplitter === "n" ? /\n/ : /\r\n/;
const lines = text.split(lineSplitterRegex);
const headers = lines[0].split(splitter); // field splitter
const headersLength = headers.length;
const linesArray = [];
for(const line of lines.slice(1))
{
const lineArray = line.split(",");
console.log(lineArray.length, lineArray);
const lineArray = line.split(splitter); // field splitter
if(lineArray.length === headersLength)
linesArray.push(lineArray);
else if(lineArray.length > 0)
linesArray.push([...lineArray, ...Array(headersLength - linesArray.length).fill("") as string[]].slice(0, headersLength));
else if(lineArray.length > headersLength) // trim excess columns
linesArray.push(lineArray.slice(0, headersLength));
else if(lineArray.length < headersLength) // add null values underfilled columns
linesArray.push([...lineArray, ...Array(headersLength - lineArray.length).fill("") as string[]]);
}
columns = Array(headersLength).fill(null);
columns = Array(headersLength).fill("");
return {
headers,
lines: linesArray
}
}
</script>

<svelte:head>
<title>Article — Import</title>
</svelte:head>

{#if form?.import !== undefined && "error" in form.import}
<Modal title={$_('app.generic.error')} on:close={() => form = null}>
<p>{$_(form.import.error)}</p>
</Modal>
{/if}

{#if form?.import !== undefined && "warnings" in form.import}
<Modal title={$_('app.generic.warning')} on:close={() => form = null}>
<ul>
{#each form.import.warnings as warning}
<li>{warning}</li>
{/each}
</ul>
<div slot="form">
<Button size="small" role="warning">{$_('app.action.validate')}</Button>
</div>
</Modal>
{/if}

<h2>Importer des articles</h2>
<p>Importer des articles a l'aide d'un fichier CSV.</p>

<form action="?/import" method="post" class="mt-8" enctype='multipart/form-data'>
<form action="?/import" method="post" class="mt-8" enctype='multipart/form-data' on:submit={() => importSuspense = true}>
<Flex direction="col" class="w-1/3">

<FormInput type="select" name="col_splitter" label="Séparateur de champs" values={[",", ";"]} bind:value={splitter}>
<option value=",">Virgule</option>
<option value=";">Point-virgule</option>
</FormInput>

<FormInput type="select" name="row_splitter" label="Séparateur de lignes" values={["n", "rn"]} bind:value={rowSplitter}>
<option value="n">Saut de ligne (\n)</option>
<option value="rn">Windows CRLF (\r\n)</option>
</FormInput>

<input type="file" name="file" on:change={(e)=> {
file = e.target?.files.item(0);
}} />

<input type="hidden" name="columnOrder" value={JSON.stringify(columns)} />
<input type="hidden" name="column_order" value={JSON.stringify(columns)} />

{#if file !== undefined}
<Button class="self-start mb-6">Importer</Button>
<Button class="self-start mb-6" suspense={importSuspense}>Importer</Button>
{/if}

</Flex>
</form>

{#if file !== undefined}
{#await parseCSV(file)}
<p>Parsing CSV file</p>
{#await parseCSV(file, splitter)}
<p>Parsing CSV file...</p>
{:then parsed}
<Table headers={parsed.headers.map(h => ({ label: h }))}>
{#each parsed.headers as _header, i}
{#each parsed.headers as header, i (header)}
<TableCell>
<FormInput type="select" name="column#{i}" bind:value={columns[i]}>
<option value={null}>Ignorer</option>
{#each availableColumns.filter(ac => !(columns.filter((column, j) => i !== j).includes(ac))) as ac}
<option value={ac}>{ac}</option>
<FormInput type="select" label="Colone correspondante" name="header-selector-{i}" bind:value={columns[i]}>
<option value={""}>Ignorer</option>

{#each availableColumns as ac}
<option value={ac} disabled={columns.includes(ac)}>{$_('app.generic.' + ac)}</option>
{/each}
</FormInput>
</TableCell>
Expand Down

0 comments on commit 3341b7a

Please sign in to comment.