{"id":94,"date":"2025-05-21T09:34:49","date_gmt":"2025-05-21T09:34:49","guid":{"rendered":"https:\/\/bjornproost.nl\/?page_id=94"},"modified":"2025-09-27T01:57:17","modified_gmt":"2025-09-27T01:57:17","slug":"voorraad-tellingen","status":"publish","type":"page","link":"https:\/\/bjornproost.nl\/?page_id=94","title":{"rendered":"Voorraad tellingen"},"content":{"rendered":"\n<p><strong>1. Voorraadtellingen standaardiseren in boekhoudsoftware<\/strong><\/p>\n\n\n\n<p class=\"has-primary-color has-text-color has-link-color wp-elements-bf044549ca94cb98934e39c680f09009\">In veel kantoren is de jaarlijkse of periodieke voorraadtelling nog steeds een handmatig en foutgevoelig proces. Medewerkers lopen met papieren lijsten of Excel-sheets door het magazijn, vullen aantallen in en leveren die later weer aan bij de administratie. Pas achteraf \u2013 vaak weken later \u2013 worden de tellingen handmatig verwerkt in het boekhoudsysteem. Het gevolg: dubbele invoer, fouten, vertraging in de rapportage en weinig inzicht tijdens het proces.<\/p>\n\n\n\n<p>Een standaardoplossing kan dit hele traject vereenvoudigen en professionaliseren:<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Het probleem met handmatige tellingen<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Dubbele invoer:<\/strong> gegevens worden eerst fysiek genoteerd en later nog eens ingevoerd in de administratie.<\/li>\n\n\n\n<li><strong>Foutgevoelig:<\/strong> een verkeerd cijfer, verkeerd product of een vergeten regel kan direct leiden tot verkeerde voorraadstanden en foutieve jaarcijfers.<\/li>\n\n\n\n<li><strong>Geen real-time inzicht:<\/strong> pas als de boekhouder alle gegevens heeft verwerkt, wordt zichtbaar wat de waarde van de voorraad is.<\/li>\n\n\n\n<li><strong>Tijdsverlies:<\/strong> de verwerking kost vaak uren tot dagen, waardoor de financi\u00eble rapportages achterlopen.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>De standaardoplossing: digitaal tellen + directe boekhoudkoppeling<\/strong><\/h3>\n\n\n\n<p>Door een <strong>eenvoudige interface<\/strong> te ontwikkelen \u2013 bijvoorbeeld een webapp of mobiele tool \u2013 worden de tellingen digitaal ingevoerd en direct gekoppeld aan het boekhoudpakket.<\/p>\n\n\n\n<p>Zo werkt het:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Medewerker logt in via laptop, tablet of smartphone.<\/li>\n\n\n\n<li>Selecteert het juiste magazijn of productcategorie.<\/li>\n\n\n\n<li>Voert het getelde aantal in; eventueel met barcode- of QR-scanner.<\/li>\n\n\n\n<li>Het systeem berekent automatisch de totale voorraadwaarde op basis van de boekhoudkundige inkoopprijzen.<\/li>\n\n\n\n<li>Met \u00e9\u00e9n klik exporteer je een XML-bestand of UBL-post die rechtstreeks wordt ingelezen in het boekhoudpakket.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Voordelen voor het kantoor<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Voorspelbaarheid:<\/strong> het proces is eenduidig en herhaalbaar, waardoor elk teamlid op dezelfde manier werkt.<\/li>\n\n\n\n<li><strong>Tijdsbesparing:<\/strong> dubbele handelingen verdwijnen; het tellen en verwerken gebeurt in \u00e9\u00e9n stap.<\/li>\n\n\n\n<li><strong>Betere controle:<\/strong> real-time inzicht in aantallen, waarde en afwijkingen maakt het eenvoudiger om fouten direct te corrigeren.<\/li>\n\n\n\n<li><strong>Hogere datakwaliteit:<\/strong> doordat de cijfers direct in het boekhoudsysteem belanden, is de kans op menselijke fouten minimaal.<\/li>\n\n\n\n<li><strong>Rapportage zonder vertraging:<\/strong> het resultaat is dezelfde dag nog beschikbaar voor management en accountant.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>De rol van visie en strategie<\/strong><\/h3>\n\n\n\n<p>Deze oplossing is geen losse tool, maar een <strong>standaardproces dat voortkomt uit een bredere strategie<\/strong>: repetitieve taken automatiseren, zodat professionals zich kunnen richten op interpretatie en advies.<br>Het invoeren van digitale voorraadtellingen past bij de visie dat kantoren data-gedreven en voorspelbaar moeten kunnen werken.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<!DOCTYPE html>\n<html lang=\"nl\">\n<head>\n  <meta charset=\"UTF-8\" \/>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" \/>\n  <title>Voorraadtelling Applicatie<\/title>\n  <script src=\"https:\/\/cdn.tailwindcss.com\"><\/script>\n  <script src=\"https:\/\/unpkg.com\/react@18\/umd\/react.development.js\" crossorigin><\/script>\n  <script src=\"https:\/\/unpkg.com\/react-dom@18\/umd\/react-dom.development.js\" crossorigin><\/script>\n  <script src=\"https:\/\/unpkg.com\/@babel\/standalone\/babel.min.js\"><\/script>\n<\/head>\n<body class=\"bg-gray-50\">\n  <div id=\"root\"><\/div>\n  <script type=\"text\/babel\">\n    const { useState, useMemo, useCallback } = React;\n\n    \/\/ --- XML Service ---\n    const escapeXml = (unsafe) => {\n        if (typeof unsafe !== 'string') return '';\n        return unsafe.replace(\/[<>&'\"]\/g, (c) => {\n            switch (c) {\n                case '<': return '&lt;';\n                case '>': return '&gt;';\n                case '&': return '&amp;';\n                case '\\'': return '&apos;';\n                case '\"': return '&quot;';\n                default: return c;\n            }\n        });\n    };\n\n    const generateXml = (items, recipientEmail) => {\n        const now = new Date();\n        const issueDate = now.toISOString().split('T')[0];\n        const issueTime = now.toTimeString().split(' ')[0];\n        const totalAmount = items.reduce((sum, item) => sum + item.totalValue, 0).toFixed(2);\n\n        const lineItemsXml = items.map((item, index) => `\n        <cac:InventoryReportLine>\n          <cbc:ID>${index + 1}<\/cbc:ID>\n          <cbc:Note>Voorraadtelling<\/cbc:Note>\n          <cbc:InvoicedQuantity unitCode=\"XU\">${item.quantity}<\/cbc:InvoicedQuantity>\n          <cbc:LineExtensionAmount currencyID=\"EUR\">${item.totalValue.toFixed(2)}<\/cbc:LineExtensionAmount>\n          <cac:Item>\n            <cbc:Description>${escapeXml(item.description)}<\/cbc:Description>\n            <cbc:Name>${escapeXml(item.description)}<\/cbc:Name>\n            <cac:SellersItemIdentification>\n              <cbc:ID>${escapeXml(item.articleCode) || 'N\/A'}<\/cbc:ID>\n            <\/cac:SellersItemIdentification>\n          <\/cac:Item>\n          <cac:Price>\n            <cbc:PriceAmount currencyID=\"EUR\">${item.unitValue.toFixed(2)}<\/cbc:PriceAmount>\n          <\/cac:Price>\n        <\/cac:InventoryReportLine>`).join('');\n\n        return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n    <InventoryReport xmlns=\"urn:oasis:names:specification:ubl:schema:xsd:InventoryReport-2\"\n                     xmlns:cac=\"urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2\"\n                     xmlns:cbc=\"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2\">\n      <cbc:UBLVersionID>2.1<\/cbc:UBLVersionID>\n      <cbc:ID>INV-REPORT-${now.getTime()}<\/cbc:ID>\n      <cbc:IssueDate>${issueDate}<\/cbc:IssueDate>\n      <cbc:IssueTime>${issueTime}<\/cbc:IssueTime>\n      <cbc:InventoryReportingTypeCode>StockCount<\/cbc:InventoryReportingTypeCode>\n      <cac:ReporterParty>\n        <cac:PartyName>\n          <cbc:Name>Bjorn Proost<\/cbc:Name>\n        <\/cac:PartyName>\n      <\/cac:ReporterParty>\n      <cac:ReceiverParty>\n        <cac:PartyName>\n          <cbc:Name>Ontvanger<\/cbc:Name>\n        <\/cac:PartyName>\n        <cac:Contact>\n            <cbc:ElectronicMail>${escapeXml(recipientEmail)}<\/cbc:ElectronicMail>\n        <\/cac:Contact>\n      <\/cac:ReceiverParty>\n      <cac:InventoryValue>\n          <cbc:InventoryValueAmount currencyID=\"EUR\">${totalAmount}<\/cbc:InventoryValueAmount>\n      <\/cac:InventoryValue>\n      ${lineItemsXml}\n    <\/InventoryReport>`;\n    };\n\n    \/\/ --- Icon Components ---\n    const TrashIcon = () => (\n        <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" className=\"h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n            <path fillRule=\"evenodd\" d=\"M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm4 0a1 1 0 012 0v6a1 1 0 11-2 0V8z\" clipRule=\"evenodd\" \/>\n        <\/svg>\n    );\n\n    const DownloadIcon = () => (\n      <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" className=\"-ml-1 mr-2 h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n        <path fillRule=\"evenodd\" d=\"M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z\" clipRule=\"evenodd\" \/>\n      <\/svg>\n    );\n    \n    const MailIcon = () => (\n      <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" className=\"-ml-1 mr-2 h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n        <path d=\"M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z\" \/>\n        <path d=\"M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z\" \/>\n      <\/svg>\n    );\n\n    const CheckCircleIcon = () => (\n        <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" className=\"h-6 w-6 text-green-400\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n            <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth=\"2\" d=\"M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z\" \/>\n        <\/svg>\n    );\n\n    const ExclamationCircleIcon = () => (\n        <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" className=\"h-6 w-6 text-red-400\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n            <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth=\"2\" d=\"M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" \/>\n        <\/svg>\n    );\n\n    \/\/ --- UI Components ---\n    const Notification = ({ message, type, onClose }) => {\n        const isSuccess = type === 'success';\n        const bgColor = isSuccess ? 'bg-green-50' : 'bg-red-50';\n        const textColor = isSuccess ? 'text-green-800' : 'text-red-800';\n        const borderColor = isSuccess ? 'border-green-300' : 'border-red-300';\n\n        return (\n            <div className=\"fixed top-5 right-5 z-50\">\n                <div className={`rounded-md p-4 shadow-lg border ${bgColor} ${borderColor}`}>\n                    <div className=\"flex\">\n                        <div className=\"flex-shrink-0\">\n                            {isSuccess ? <CheckCircleIcon \/> : <ExclamationCircleIcon \/>}\n                        <\/div>\n                        <div className=\"ml-3\">\n                            <p className={`text-sm font-medium ${textColor}`}>\n                                {message}\n                            <\/p>\n                        <\/div>\n                        <div className=\"ml-auto pl-3\">\n                            <div className=\"-mx-1.5 -my-1.5\">\n                                <button\n                                    type=\"button\"\n                                    onClick={onClose}\n                                    className={`inline-flex rounded-md p-1.5 ${isSuccess ? 'bg-green-50 text-green-500 hover:bg-green-100' : 'bg-red-50 text-red-500 hover:bg-red-100'} focus:outline-none focus:ring-2 focus:ring-offset-2 ${isSuccess ? 'focus:ring-offset-green-50 focus:ring-green-600' : 'focus:ring-offset-red-50 focus:ring-red-600'}`}\n                                >\n                                    <span className=\"sr-only\">Dismiss<\/span>\n                                    <svg className=\"h-5 w-5\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n                                        <path fillRule=\"evenodd\" d=\"M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z\" clipRule=\"evenodd\" \/>\n                                    <\/svg>\n                                <\/button>\n                            <\/div>\n                        <\/div>\n                    <\/div>\n                <\/div>\n            <\/div>\n        );\n    };\n\n    const Summary = ({ itemCount, totalValue }) => (\n        <div className=\"my-8 p-6 bg-blue-50 border border-blue-200 rounded-lg flex justify-between items-center\">\n            <div>\n                <h3 className=\"text-lg font-semibold text-blue-800\">Totaal Overzicht<\/h3>\n                <p className=\"text-blue-600\">{itemCount} {itemCount === 1 ? 'item' : 'items'} in de lijst.<\/p>\n            <\/div>\n            <div className=\"text-right\">\n                 <p className=\"text-sm text-blue-700\">Totale Waarde<\/p>\n                 <p className=\"text-3xl font-bold text-blue-900\">\u20ac{totalValue.toFixed(2)}<\/p>\n            <\/div>\n        <\/div>\n    );\n\n    const ExportSection = ({ email, onEmailChange, onDownload, onSendEmail }) => (\n        <div className=\"mt-12 pt-8 border-t border-gray-200\">\n            <h2 className=\"text-2xl font-semibold mb-4 text-gray-700\">Exporteren<\/h2>\n            <div className=\"max-w-md\">\n                <label htmlFor=\"emailAdres\" className=\"block text-sm font-medium text-gray-700\">E-mailadres voor verzending<\/label>\n                <input\n                    id=\"emailAdres\"\n                    type=\"email\"\n                    value={email}\n                    onChange={(e) => onEmailChange(e.target.value)}\n                    placeholder=\"jouwemail@voorbeeld.com\"\n                    className=\"mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm text-gray-900\"\n                \/>\n            <\/div>\n            <div className=\"flex items-center space-x-4 mt-4\">\n                <button\n                    onClick={onDownload}\n                    className=\"inline-flex items-center px-5 py-2.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500\"\n                >\n                    <DownloadIcon \/>\n                    Download XML\n                <\/button>\n                <button\n                    onClick={onSendEmail}\n                    className=\"inline-flex items-center px-5 py-2.5 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500\"\n                >\n                    <MailIcon \/>\n                    Verstuur via E-mail\n                <\/button>\n            <\/div>\n        <\/div>\n    );\n\n    const InventoryTable = ({ items, onDelete }) => {\n        if (items.length === 0) {\n            return <p id=\"noItemsMessage\" className=\"mt-4 mb-6 text-center text-gray-500 py-8 border-2 border-dashed rounded-lg\">Nog geen items toegevoegd.<\/p>;\n        }\n\n        return (\n            <div className=\"overflow-x-auto mb-6 rounded-lg border\">\n                <table className=\"min-w-full divide-y divide-gray-200\">\n                    <thead className=\"bg-gray-50\">\n                        <tr>\n                            <th scope=\"col\" className=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Omschrijving<\/th>\n                            <th scope=\"col\" className=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Artikelcode<\/th>\n                            <th scope=\"col\" className=\"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\">Aantal<\/th>\n                            <th scope=\"col\" className=\"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\">Waarde\/stuk (\u20ac)<\/th>\n                            <th scope=\"col\" className=\"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\">Totaal Waarde (\u20ac)<\/th>\n                            <th scope=\"col\" className=\"relative px-6 py-3\">\n                                <span className=\"sr-only\">Acties<\/span>\n                            <\/th>\n                        <\/tr>\n                    <\/thead>\n                    <tbody className=\"bg-white divide-y divide-gray-200\">\n                        {items.map((item) => (\n                            <tr key={item.id} className=\"hover:bg-gray-50\">\n                                <td className=\"px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900\">{item.description}<\/td>\n                                <td className=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">{item.articleCode || '-'}<\/td>\n                                <td className=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right\">{item.quantity}<\/td>\n                                <td className=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right\">{item.unitValue.toFixed(2)}<\/td>\n                                <td className=\"px-6 py-4 whitespace-nowrap text-sm font-semibold text-gray-800 text-right\">{item.totalValue.toFixed(2)}<\/td>\n                                <td className=\"px-6 py-4 whitespace-nowrap text-right text-sm font-medium\">\n                                    <button\n                                        onClick={() => onDelete(item.id)}\n                                        className=\"text-red-600 hover:text-red-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 rounded-md p-1\"\n                                        aria-label={`Verwijder ${item.description}`}\n                                    >\n                                        <TrashIcon \/>\n                                    <\/button>\n                                <\/td>\n                            <\/tr>\n                        ))}\n                    <\/tbody>\n                <\/table>\n            <\/div>\n        );\n    };\n\n    const InputRow = ({ onAddItem }) => {\n        const initialInputState = {\n            description: '',\n            articleCode: '',\n            quantity: 0,\n            unitValue: 0,\n        };\n\n        const [input, setInput] = useState(initialInputState);\n        const [errors, setErrors] = useState({});\n\n        const handleInputChange = (field, value) => {\n            setInput(prev => ({ ...prev, [field]: value }));\n            if (errors[field]) {\n                setErrors(prev => {\n                    const newErrors = { ...prev };\n                    delete newErrors[field];\n                    return newErrors;\n                });\n            }\n        };\n        \n        const rowTotal = useMemo(() => {\n            const quantity = Number(input.quantity) || 0;\n            const unitValue = Number(input.unitValue) || 0;\n            return (quantity * unitValue).toFixed(2);\n        }, [input.quantity, input.unitValue]);\n\n        const validate = () => {\n            const newErrors = {};\n            if (!input.description.trim()) {\n                newErrors.description = \"Omschrijving is verplicht.\";\n            }\n            if (Number(input.quantity) <= 0) {\n                newErrors.quantity = \"Aantal moet groter dan 0 zijn.\";\n            }\n            if (Number(input.unitValue) <= 0) {\n                newErrors.unitValue = \"Waarde moet groter dan 0 zijn.\";\n            }\n            setErrors(newErrors);\n            return Object.keys(newErrors).length === 0;\n        };\n\n        const handleAdd = () => {\n            if (validate()) {\n                onAddItem({\n                    ...input,\n                    quantity: Number(input.quantity),\n                    unitValue: Number(input.unitValue)\n                });\n                setInput(initialInputState);\n            }\n        };\n        \n        const handleKeyDown = (e) => {\n            if (e.key === 'Enter') {\n                handleAdd();\n            }\n        };\n\n        return (\n            <div className=\"bg-gray-50 p-4 rounded-lg border my-6\">\n                <h2 className=\"text-xl font-semibold mb-3 text-gray-700\">Nieuw Item Toevoegen<\/h2>\n                <div className=\"grid grid-cols-1 md:grid-cols-12 gap-4 items-start\">\n                    <div className=\"md:col-span-3\">\n                        <label htmlFor=\"inputOmschrijving\" className=\"block text-sm font-medium text-gray-700 mb-1\">Omschrijving<\/label>\n                        <input id=\"inputOmschrijving\" value={input.description} onChange={(e) => handleInputChange('description', e.target.value)} onKeyDown={handleKeyDown} className={`w-full p-2 border rounded-md text-gray-900 ${errors.description ? 'border-red-500' : 'border-gray-300'}`} type=\"text\" placeholder=\"Productomschrijving\"\/>\n                        {errors.description && <div className=\"text-red-600 text-xs mt-1\">{errors.description}<\/div>}\n                    <\/div>\n                    <div className=\"md:col-span-2\">\n                        <label htmlFor=\"inputArtikelcode\" className=\"block text-sm font-medium text-gray-700 mb-1\">Artikelcode<\/label>\n                        <input id=\"inputArtikelcode\" value={input.articleCode} onChange={(e) => handleInputChange('articleCode', e.target.value)} onKeyDown={handleKeyDown} className=\"w-full p-2 border border-gray-300 rounded-md text-gray-900\" type=\"text\" placeholder=\"Optioneel\"\/>\n                    <\/div>\n                    <div className=\"md:col-span-2\">\n                        <label htmlFor=\"inputAantal\" className=\"block text-sm font-medium text-gray-700 mb-1\">Aantal<\/label>\n                        <input id=\"inputAantal\" value={input.quantity || ''} onChange={(e) => handleInputChange('quantity', Number(e.target.value))} onKeyDown={handleKeyDown} className={`w-full p-2 border rounded-md text-gray-900 ${errors.quantity ? 'border-red-500' : 'border-gray-300'}`} min=\"0\" type=\"number\" placeholder=\"0\"\/>\n                        {errors.quantity && <div className=\"text-red-600 text-xs mt-1\">{errors.quantity}<\/div>}\n                    <\/div>\n                    <div className=\"md:col-span-2\">\n                        <label htmlFor=\"inputWaarde\" className=\"block text-sm font-medium text-gray-700 mb-1\">Waarde\/stuk (\u20ac)<\/label>\n                        <input id=\"inputWaarde\" value={input.unitValue || ''} onChange={(e) => handleInputChange('unitValue', Number(e.target.value))} onKeyDown={handleKeyDown} className={`w-full p-2 border rounded-md text-gray-900 ${errors.unitValue ? 'border-red-500' : 'border-gray-300'}`} min=\"0\" step=\"0.01\" type=\"number\" placeholder=\"0.00\"\/>\n                        {errors.unitValue && <div className=\"text-red-600 text-xs mt-1\">{errors.unitValue}<\/div>}\n                    <\/div>\n                    <div className=\"md:col-span-1 text-right\">\n                        <label className=\"block text-sm font-medium text-gray-700 mb-1\">Totaal<\/label>\n                        <p className=\"p-2 text-gray-800 font-semibold\">{rowTotal}<\/p>\n                    <\/div>\n                    <div className=\"md:col-span-2 self-end\">\n                        <button onClick={handleAdd} className=\"w-full bg-blue-600 text-white font-bold py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors duration-200\">\n                            Voeg Toe\n                        <\/button>\n                    <\/div>\n                <\/div>\n            <\/div>\n        );\n    };\n\n    \/\/ --- Main App Component ---\n    const App = () => {\n        const [items, setItems] = useState([]);\n        const [email, setEmail] = useState('');\n        const [notification, setNotification] = useState(null);\n\n        const showNotification = (message, type) => {\n          setNotification({ message, type });\n          setTimeout(() => setNotification(null), 4000);\n        };\n\n        const handleAddItem = useCallback((newItem) => {\n            const fullItem = {\n                ...newItem,\n                id: Date.now(),\n                totalValue: newItem.quantity * newItem.unitValue,\n            };\n            setItems(prevItems => [...prevItems, fullItem]);\n            showNotification(`'${newItem.description}' toegevoegd.`, 'success');\n        }, []);\n\n        const handleDeleteItem = useCallback((id) => {\n            const itemToDelete = items.find(item => item.id === id);\n            setItems(prevItems => prevItems.filter(item => item.id !== id));\n            if(itemToDelete){\n                showNotification(`'${itemToDelete.description}' verwijderd.`, 'error');\n            }\n        }, [items]);\n\n        const grandTotal = useMemo(() => {\n            return items.reduce((total, item) => total + item.totalValue, 0);\n        }, [items]);\n\n        const validateExport = useCallback(() => {\n            if (!email || !\/^\\S+@\\S+\\.\\S+$\/.test(email)) {\n                showNotification('Voer een geldig e-mailadres in.', 'error');\n                return false;\n            }\n            if (items.length === 0) {\n                showNotification('Voeg eerst items toe aan de lijst.', 'error');\n                return false;\n            }\n            return true;\n        }, [email, items]);\n        \n        const handleDownload = useCallback(() => {\n            if (!validateExport()) return;\n\n            const xml = generateXml(items, email);\n            const blob = new Blob([xml], { type: 'application\/xml;charset=utf-8' });\n            const url = URL.createObjectURL(blob);\n            const a = document.createElement('a');\n            a.href = url;\n            const timestamp = new Date().toISOString().replace(\/[:.]\/g, '-');\n            a.download = `voorraadtelling-${timestamp}.xml`;\n            document.body.appendChild(a);\n            a.click();\n            document.body.removeChild(a);\n            URL.revokeObjectURL(url);\n            showNotification('XML-bestand succesvol gedownload.', 'success');\n        }, [items, email, validateExport]);\n\n        const handleSendEmail = useCallback(() => {\n            if (!validateExport()) return;\n            \n            const subject = `Voorraadtelling ${new Date().toLocaleDateString('nl-NL')}`;\n            const body = `Beste,\\n\\nGelieve het zojuist gedownloade XML-bestand met de voorraadtelling als bijlage aan deze e-mail toe te voegen.\\n\\nMet vriendelijke groet,\\n\\nBjorn Proost`;\n            const mailtoLink = `mailto:${email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;\n            window.location.href = mailtoLink;\n        }, [email, items, validateExport]);\n\n\n        return (\n            <div className=\"container mx-auto p-4 sm:p-6 lg:p-8 font-sans\">\n                {notification && <Notification message={notification.message} type={notification.type} onClose={() => setNotification(null)} \/>}\n                <div className=\"bg-white p-8 rounded-2xl shadow-lg\">\n                    <header className=\"mb-8 border-b pb-4\">\n                        <h1 className=\"text-4xl font-bold text-gray-800\">Voorraadtelling<\/h1>\n                        <p className=\"text-gray-500 mt-1\">Voer hier de getelde voorraad in.<\/p>\n                    <\/header>\n\n                    <main>\n                        <InventoryTable items={items} onDelete={handleDeleteItem} \/>\n                        <InputRow onAddItem={handleAddItem} \/>\n\n                        {items.length > 0 && <Summary totalValue={grandTotal} itemCount={items.length} \/>}\n\n                        <ExportSection\n                            email={email}\n                            onEmailChange={setEmail}\n                            onDownload={handleDownload}\n                            onSendEmail={handleSendEmail}\n                        \/>\n                    <\/main>\n                <\/div>\n            <\/div>\n        );\n    };\n\n    \/\/ --- Render App ---\n    const rootElement = document.getElementById('root');\n    const root = ReactDOM.createRoot(rootElement);\n    root.render(<App \/>);\n  <\/script>\n<\/body>\n<\/html>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Stap naar bredere integratie<\/strong><\/h3>\n\n\n\n<p>De voorraadtelling-interface kan worden uitgebreid met:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>koppeling met kassasystemen of ERP-software voor directe voorraadmutaties;<\/li>\n\n\n\n<li>waarschuwingen bij grote afwijkingen ten opzichte van de boekhouding;<\/li>\n\n\n\n<li>dashboards voor trendanalyse en seizoenspatronen;<\/li>\n\n\n\n<li>automatische audit-trails voor interne en externe controles.<\/li>\n<\/ul>\n\n\n\n<p>Zo groeit een relatief eenvoudige oplossing uit tot een belangrijke schakel in de datagedreven bedrijfsvoering van het kantoor en de ondernemer.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><strong>Conclusie:<\/strong><br>Door voorraadtellingen te standaardiseren en direct te koppelen aan het boekhoudsysteem verandert een tijdrovend, foutgevoelig proces in een voorspelbare en controleerbare workflow. Het bespaart tijd, verhoogt de datakwaliteit en legt de basis voor verdergaande automatisering en adviesgerichte dienstverlening.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>1. Voorraadtellingen standaardiseren in boekhoudsoftware In veel kantoren is de jaarlijkse of periodieke voorraadtelling nog steeds een handmatig en foutgevoelig proces. Medewerkers lopen met papieren lijsten of Excel-sheets door het magazijn, vullen aantallen in en leveren die later weer aan bij de administratie. Pas achteraf \u2013 vaak weken later \u2013 worden de tellingen handmatig verwerkt [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"parent":39,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_joinchat":[],"footnotes":""},"class_list":["post-94","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/bjornproost.nl\/index.php?rest_route=\/wp\/v2\/pages\/94","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/bjornproost.nl\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/bjornproost.nl\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/bjornproost.nl\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/bjornproost.nl\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=94"}],"version-history":[{"count":9,"href":"https:\/\/bjornproost.nl\/index.php?rest_route=\/wp\/v2\/pages\/94\/revisions"}],"predecessor-version":[{"id":204,"href":"https:\/\/bjornproost.nl\/index.php?rest_route=\/wp\/v2\/pages\/94\/revisions\/204"}],"up":[{"embeddable":true,"href":"https:\/\/bjornproost.nl\/index.php?rest_route=\/wp\/v2\/pages\/39"}],"wp:attachment":[{"href":"https:\/\/bjornproost.nl\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=94"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}