Slovo-Plugin-Prodan/lib/Slovo/Plugin/Prodan.pm
package Slovo::Plugin::Prodan;
use feature ':5.26';
use Mojo::Base 'Mojolicious::Plugin', -signatures;
use Mojo::JSON qw(true false);
our $AUTHORITY = 'cpan:BEROV';
our $VERSION = '0.05';
has app => sub { Slovo->new }, weak => 1;
sub register ($self, $app, $conf) {
$self->app($app);
# Prepend class
unshift @{$app->renderer->classes}, __PACKAGE__;
unshift @{$app->static->classes}, __PACKAGE__;
$app->stylesheets('/css/cart.css');
$app->javascripts('/js/cart.js');
$app->config->{consents}{gdpr_url}
= $conf->{consents}{gdpr_url} || '/ѿносно/условия.bg.html';
$app->config->{consents}{phone_url} = $conf->{consents}{phone_url} || '+359899999999';
$app->config->{consents}{delivery_prices_url} ||= '/ѿносно/цени-доставка.bg.html';
# $app->log->debug(join $/, sort keys %INC);
# $app->debug('Prodan $config', $conf);
# Set this flag, when we have changes to the tables to be applied.
$self->_migrate($app, $conf) if $conf->{migrate};
my $spec = $app->openapi_spec;
%{$spec->{definitions}} = (%{$spec->{definitions}}, $self->_definitions);
%{$spec->{paths}} = (%{$spec->{paths}}, $self->_paths);
# Add new data_type for celina. Now a corresponding partial template can be
# used to render the new data_type.
push @{$spec->{parameters}{data_type}{enum}}, '_consents';
$app->plugin(OpenAPI => {spec => $spec});
# $app->debug($spec);
# Generate helpers for instantiating Slovo::Model classes just like
# Slovo::PLugin::MojoDBx
for my $t ('poruchki', 'products') {
my $T = Mojo::Util::camelize($t);
my $class = "Slovo::Model::$T";
$app->load_class($class);
$app->helper(
$t => sub ($c) {
my $m = $class->new(dbx => $c->dbx, c => $c);
Scalar::Util::weaken $m->{c};
return $m;
});
}
# configure deliverers
$self->_configure_deliverers($conf);
return $self;
}
sub _paths {
return (
'/poruchki' => {
post => {
description => 'Create a new order',
'x-mojo-to' => 'poruchki#store',
parameters => [{
required => true,
in => 'body',
name => 'Poruchka',
schema => {'$ref' => '#/definitions/Poruchka'}
},
],
responses => {
201 => {
description => 'Order created successfully!',
schema => {'$ref' => '#/definitions/Poruchka'}
},
default => {'$ref' => '#/definitions/ErrorResponse'}}}
},
'/poruchka/:deliverer/:deliverer_id' => {
put => {
description => 'show an order by given :deliverer and :id with that deliverer. ',
'x-mojo-to' => 'poruchki#show',
parameters => [{
name => 'deliverer_id',
in => 'path',
description => 'Id of the order in deliverer\'s system',
type => 'integer',
required => true,
},
{
name => 'deliverer',
in => 'path',
description => 'A string which denotes the deliverer. Example: "email". ',
type => 'string',
enum => [qw(email econt)],
required => true,
},
{
name => 'id',
in => 'formData',
description =>
'"id" property, found in the order structure. Generated by the Econt delivery confirmation form',
type => 'string',
required => true,
},
],
responses => {
200 => {
description =>
'Show eventually updated order from :deliverer with :deliverer_id',
schema => {'$ref' => '#/definitions/Poruchka'}
},
default => {'$ref' => '#/definitions/ErrorResponse'}}}
},
'/shop' => {
get => {
description => 'Provides data for the shop',
'x-mojo-to' => 'poruchki#shop',
responses => {default => {'$ref' => '#/definitions/ErrorResponse'}}}
},
'/consents' => {
get => {
description => 'Page URL for GDPR concent',
'x-mojo-to' => 'poruchki#consents',
responses => {
200 => {
description => 'The URL to the detailed usage conditions, GDPR, cookies.',
schema => {'$ref' => '#/definitions/Consents'}
},
default => {'$ref' => '#/definitions/ErrorResponse'}}}
},
);
}
# Returns description as a perl structure of objects defined for the json API
# to be added to the /definitions of our OpenAPI
sub _definitions {
return (
ListOfPoruchki => {
description => 'An array of Poruchka items.',
items => {'$ref' => '#/definitions/Poruchka', type => 'array',}
},
Poruchka => {
properties => {
deliverer_id => {
description =>
'Id of the order as given by Econt, returned with the response to the user-agent(browser)',
type => 'integer',
},
name => {maxLength => 100, type => 'string',},
email => {maxLength => 100, minLength => 0, type => 'string',},
phone => {maxLength => 20, type => 'string',},
deliverer => {maxLength => 100, type => 'string',},
city_name => {maxLength => 55, type => 'string'},
address => {maxLength => 155, type => 'string'},
notes => {maxLength => 255, type => 'string'},
items => {'$ref' => '#/definitions/OrderProducts', type => 'array',},
way_bill_id => {
description =>
'Id at the deliverer site, returned by their system after we created the way-bill at their site.',
maxLength => 40,
type => 'string'
},
executed => {
type => 'integer',
minimum => 0,
maximum => 9,
description => 'Level of execution of the order.0:registered',
}
},
},
OrderProducts => {
description => 'An array of OrderProduct items in an order.',
items => {
'$ref' => '#/definitions/OrderProduct',
type => 'array',
}
},
OrderProduct => {
description => 'An item in an order (cart): sku, title, quantity, price',
properties => {
sku => {maxLength => 40, type => 'string'},
title => {maxLength => 155, type => 'string'},
quantity => {type => 'integer'},
weight => {type => 'number'},
price => {type => 'number'},
}
},
Consents => {
properties => {
ihost => {type => 'string'},
gdpr_url => {type => 'string'},
delivery_prices_url => {type => 'string'}
},
required => [qw(gdpr_url ihost delivery_prices_url)],
},
);
}
# Create tables in the database on the very first run if they do not exist.
sub _migrate ($self, $app, $conf) {
$app->dbx->migrations->migrate;
$app->dbx->migrations->name('prodan')
->from_data(__PACKAGE__, 'resources/data/prodan_migrations.sql')->migrate();
return $self;
}
# Deliverers are companies which deliver goods to users of our online shop.
# Such delivereres in Bulgaria are Econt, Speedy, Bulgarian Posts and others.
# Currently we integrate only Econt
my sub DELIVERERS {
return qw(econt);
}
sub _configure_deliverers ($self, $conf) {
for my $d (DELIVERERS) {
my $d_sub = '_configure_' . $d;
$self->$d_sub($conf);
}
return;
}
# The keys in the $conf hash reference are named after the examples given at
# http://delivery.econt.com/services/
sub _configure_econt ($self, $conf) {
my $eco = $conf->{econt};
# ID на магазина в "Достави с Еконт"
$eco->{shop_id} //= 'demo';
# Код за свързване
$eco->{private_key} //= 'demo';
# валута на магазина (валута на наложения платеж)
$eco->{shop_currency} //= 'BGN';
# URL визуализиращ форма за доставка
$eco->{shippment_calc_url} //= 'https://delivery-demo.econt.com/customer_info.php';
# Ендпойнта на услугата за създаване или редактиране на поръчка
$eco->{crupdate_order_endpoint}
//= 'https://delivery-demo.econt.com/services/OrdersService.updateOrder.json';
# Ендпойнта на услугата за създаване или редактиране на товарителница
$eco->{create_awb_endpoint}
//= 'https://delivery-demo.econt.com/services/OrdersService.createAWB.json';
# $self->app->debug($conf);
$self->app->config->{shop} = $eco;
return;
}
1;
=encoding utf8
=head1 NAME
Slovo::Plugin::Prodan - Make and manage sales in your Slovo-based site
=head1 SYNOPSIS
# In slovo.conf
load_plugins => [
#...
'Themes::Malka',
{
Prodan => {
migrate => 1,
consents => {
gdpr_url => '/ѿносно/условия.bg.html',
phone_url => $ENV{SLOVO_PRODAN_PHONE_URL},
delivery_prices_url => '/ѿносно/цени-доставки.bg.html',
},
econt => {
shop_id => $ENV{SLOVO_PRODAN_SHOP_ID},
private_key => $ENV{SLOVO_PRODAN_PRIVATE_KEY},
shippment_calc_url => 'https://delivery.econt.com/customer_info.php',
crupdate_order_endpoint =>
'https://delivery.econt.com/services/OrdersService.updateOrder.json',
create_awb_endpoint =>
'https://delivery.econt.com/services/OrdersService.createAWB.json'
}}
},
#...
],
=head1 DESCRIPTION
The word про̀дан (прода̀жба) in Bulgarian means sale. Roots are found in Old
Common Slavic (Old Bulgarian) I<проданьѥ>. Here is an exerpt from Codex
Suprasliensis(331.27) where this word was witnessed: I<сꙑнъ божии. вол҄еѭ
на сьпасьнѫѭ страсть съ вами придетъ. и на B<продании> станетъ.
искѹпѹѭштааго животворьноѭ кръвьѭ. своеѭ миръ.>
L<Slovo::Plugin::Prodan> is a L<Mojolicious::Plugin> that extends a
Slovo-based site and turns it into an online shop.
=head1 FEATURES
In this edition of L<Slovo::Plugin::Prodan> we implement the following features:
=head2 A Shopping cart
A jQuery and localStorage based shopping cart. Two static files contain
the implementation and they can be inflated. The files are
C</css/cart.css> and C</js/cart.js>. You should inflate these files into
your public forlder C<domove/example.com/public> for the domain on which you
will use it. Even not inflated these will be referred from any page of the
site. The site layout C<layouts/site.html.ep> includes automatically these
two static files if this plugin is loaded.
# Inflate new static files from Slovo::Plugin::Prodan
bin/slovo inflate --class Slovo::Plugin::Prodan -p --path domove/xn--b1arjbl.xn--90ae/public
To add a product to your cart and make an order, you need a button, containing
the product data. For example:
<button class="primary sharer button add-to-cart"
title="книжно издание" data-sku="9786199169001"
data-title="Житие на света Петка Българска от свети патриарх Евтимий"
data-weight="0.5" data-price="7.00"><img
src="/css/malka/book-open-page-variant-outline.svg">
<img src="/img/cart-plus-white.svg"></button>
See "A template..." below.
=head2 Delivery of sold goods
A "Pay on delivery" integration with Bulgarian currier L<Econt (in
Bulgarian)|https://www.econt.com/developers/43-kakvo-e-dostavi-s-ekont.html>.
=head2 Products
Products - a products SQL table to populate your pages with products. You
create a page with several articles (celini) in it. These celini will be the
web-pages for the products. You prepare a YAML file with products. Each product
C<alias> property must match exactly the celina C<alias> and C<data_type> on
wich this product will be placed. See C<t/products.yaml> and
C<t/update_products.yaml> for examples. See L<Slovo::Command::prodan::products>
on how to add and update products.
=head2 Products template for books
A template for displaying products within a C<celina>. You can modify this
template as you wish to display other types of products - not just books as it
is now. See C<partials/_kniga.html.ep> inlined in this file's C<__DATA__>
section. It of course can be inflated using
L<Slovo::Command::Author::inflate>. The template produces the HTML from the
products table, including the button mentioned above already.
# Add the template form Prodan
bin/slovo inflate --class Slovo::Plugin::Prodan \
-t --path domove/xn--b1arjbl.xn--90ae/templates/themes/malka
=head2 Consents
A section in the Prodan configuration for different settings - only urls for
now. C<$app-E<gt>config('consents')> may contain any settings needed for the
client side of the plugin not related dierctly to integration with deliverers
or payment providers.
=head3 GDPR and Cookies consent
A GDPR and cookies consent alert in the footer which upon click leads to the
page (celina) where all conditions on using the site can be described. When the
user clicks on the link to the I<Consent> page a flag in C<localStorage> is put
so the alert is not shown any more. This flag disappears if the user clears any
site data and the alert will appear again if the user vists the site again.
The Consent celina is created automatically in the localhost domain as an
example. Search for C<gdpr_consent> in the source of this module to see how it
is implemented.
Settings:
Keys Default Values
--------------------------------------------
gdpr_url '/ѿносно/условия.bg.html'
ihost punycode_decode(ed) current host
=head3 Delivery prices URL
This is just a setting for this plugin - C<delivery_prices_url>. Defaults to
'/ѿносно/цени-доставка.bg.html'. This is a place where the prices for delivery
are described. The link is displayed at the bottom of the shopping cart widget.
It is created automatically for localhost as the C<gdpr_url>
=head3 phone_url
Currently displayed as a link in the _footer_right.html.ep template.
=head2 TODO some day
=over 1
=item Invoices - generate an invoice in PDF via headless LibreOffice instance
on your server.
=item Merchants - a merchants SQL table with Open API to manage and
automatically populate invoices.
=item Other "Pay on Delivery" providers. Feel free to contibute yours.
=item Other types of Payments and/or Online Payment Providers like online POS
Terminals etc.
=back
=head1 METHODS
The usual method is implemented.
=head2 register
Prepends the class to renderer and static classes. Adds some REST API routes,
configures the deliverer.
=head1 EMBEDDED FILES
@@ css/cart.css
@@ js/cart.js
@@ img/arrow-collapse-all.svg
@@ img/cart-arrow-right.svg
@@ img/cart.svg
@@ img/cart-check.svg
@@ img/cart-off.svg
@@ img/cart-minus.svg
@@ img/cart-plus-white.svg
@@ img/cart-plus.svg
@@ img/cart-remove.svg
@@ img/econt.svg
@@ partials/_footer_right.html.ep
@@ partials/_consents.html.ep
@@ partials/_kniga.html.ep
@@ resources/data/prodan_migrations.sql
=head1 SEE ALSO
L<Slovo::Command::prodan::products>,
L<Slovo>,
L<Mojolicious::Guides::Tutorial/Stash and templates>,
L<Mojolicious/renderer>,
L<Mojolicious::Renderer>,
L<Mojolicious::Guides::Rendering/Bundling assets with plugins>,
L<Slovo::Command::Author::inflate>
=head1 AUTHOR
Красимир Беров
CPAN ID: BEROV
berov на cpan точка org
http://слово.бг
=head1 CONTRIBUTORS
Ordered by time of first commit.
=over
=item * Your Name
=item * Someone Else
=item * Another Contributor
=back
=head1 COPYRIGHT
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)
The full text of the license can be found in the
LICENSE file included with this module.
This distribution contains icons from L<https://materialdesignicons.com/> and
may contain other free software which belongs to their respective authors.
=cut
__DATA__
@@ css/cart.css
/* cd ~/opt/dev/Slovo && beautify-css -p domove/xn--b1arjbl.xn--90ae/public/css/cart.css */
@charset "utf-8";
#show_cart {
background-color: var(--bg-color);
}
#cart_widget {
padding: 0;
z-index: 2;
background-color: var(--bg-color);
color: black;
/* 'position' cannot be fixed, because it must be slidable in case there
* are more products and the bottom of the table is not visible on the
* screen. */
position: absolute;
top: 8rem;
left: 0;
}
#cart_widget>h3 {
margin: 0;
}
#cart_widget>button:nth-child(1) {
float: right;
}
#cart_widget>table {
position: relative;
}
#cart_widget>table>tfoot th:nth-last-child(1),
#cart_widget>table>tbody td:nth-last-child(1) {
max-width: 13rem;
/* white-space: nowrap; */
text-align: center;
}
#last_order_items tfoot th:last-child,
#last_order_items tfoot td:last-child,
#last_order_items tbody th:last-child,
#last_order_items tbody td:last-child,
#cart_widget>table>thead th:nth-last-child(2),
#cart_widget>table>tfoot th:nth-last-child(3),
#cart_widget>table>tbody td:nth-last-child(2) {
max-width: 5rem;
white-space: nowrap;
text-align: end;
}
#econt_order_items>thead th:nth-last-child(3),
#econt_order_items>tfoot th:nth-last-child(3),
#econt_order_items>tbody td:nth-last-child(3),
#cart_widget>table>thead th:nth-last-child(3),
#cart_widget>table>tfoot th:nth-last-child(3),
#cart_widget>table>tbody td:nth-last-child(3) {
max-width: 7rem;
white-space: nowrap;
text-align: end;
}
#econt_order_items>tbody th:first-child,
#cart_widget>table>tfoot th:nth-last-child(4),
#cart_widget>table>tbody td:nth-last-child(4) {
max-width: 40rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#cart_widget>table>tfoot th:nth-last-child(4) {
text-align: right;
}
button.product,
.button.product,
.button.cart,
button.cart {
font-family: FreeSans, sans-serif;
color: var(--color-success);
font-weight: bolder;
border-radius: 4px;
font-size: small;
padding: .2rem .2em;
}
#cart_widget tr:nth-child(even) td {
background-color: var(--color-lightGrey);
}
#cart_widget tr:nth-child(odd) td {
background-color: white;
}
img.outline {
color: var(--color-primary);
border: 1px solid var(--color-primary);
border-radius: 4px;
cursor: pointer;
}
#econt_order_layer,
#email_order_layer,
#last_order_layer {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 3;
background-color: rgba(250, 250, 250, 0.80);
}
#last_order_items div {
font-family: sans-serif;
}
/*
*/
#order_help h5,
#order_help h6 {
margin: 0.35rem 0 0 0;
}
#email_order_form input:invalid {
border: red solid 2px;
}
#order_help p {
font-family: Veleka, serif;
}
#order_help {
z-index: 4;
position: absolute;
top: 0;
left: 30%;
right: 30%;
}
#last_order_table div.col:first-child {
font-weight: bolder;
}
#last_order_table div.col:nth-child(2) {
font-weight: normal;
font-family: sans-serif;
}
/*econt form iframe*/
iframe#econt_shipment {
width: 100%;
height: 600px;
}
#econt_order_layer .card {
padding: 1rem;
}
#econt_order_items th,
#econt_order_items td,
#cart_widget th,
#cart_widget td,
#last_order_layer th,
#last_order_layer td {
padding: 0.5rem !important;
line-height: 85%;
}
@media (max-width: 700px) {
/* .plus, .minus,.remove images as buttons */
#cart_widget img.outline {
width: 32px;
}
#econt_order_items th:nth-last-child(1),
#econt_order_items td:nth-last-child(1),
#econt_order_items tbody th:nth-last-child(2),
#econt_order_items tbody td:nth-last-child(2),
#econt_order_items tbody th:nth-last-child(3),
#econt_order_items tbody td:nth-last-child(3),
#last_order_items tfoot th:last-child,
#last_order_items tfoot td:last-child,
#cart_widget>table>tfoot th:nth-last-child(1),
#cart_widget>table>tbody td:nth-last-child(1) {
max-width: 5rem;
text-align: end;
}
#econt_order_items>tbody th:first-child,
#last_order_items th:first-child,
#econt_order_items th:first-child,
#econt_order_items td:first-child,
#last_order_items td:nth-last-child(1),
#cart_widget>table>tfoot th:nth-last-child(4),
#cart_widget>table>tbody td:nth-last-child(4) {
max-width: 10rem;
}
.remove {
display: none;
}
#order_help {
top: 0;
left: 0;
right: 0;
}
}
.button.primary.sharer {
font-size: 80%;
}
/* end @media (max-width: 700px) */
/* books (products) */
section.book figure {
margin: 1rem;
clear: both;
}
section.book figure>img {
max-width: 200px;
max-height: 288px;
border: 1px solid black;
}
section.book figure>figcaption {
position: relative;
bottom: 0;
}
section.book table#meta {
width: 100%;
}
section.book table#meta th {
max-width: 7rem;
min-width: 5rem;
}
section.book table#meta th,
table#meta td {
vertical-align: top;
border-bottom: 1px solid #ddd;
padding: 0.4rem 0.8rem;
}
section.book table#meta tr:last-child th,
table#meta tr:last-child td {
border-bottom: none;
}
section.book table#meta tr.separator {
border-top: 2px solid var(--color-lightGrey);
}
/* other books */
section.book div.row.text-left div.col.card {
height: 128px;
min-width: 12rem;
max-width: 12rem;
}
section.book div.row.text-left div.col.card img {
height: 110px;
}
.button.primary.sharer {
font-weight: bolder;
border-radius: 4px;
padding: .1rem .5rem;
display: inline-block;
vertical-align: text-top;
}
.social {
white-space: nowrap;
}
section.book h2 {
margin: 0;
}
.referrer {
color: var(--color-primary);
}
/* end books (products) */
@@ js/cart.js
/* An unobtrusive shopping cart based on localStorage
* Formatted with `js-beautify -j -r -f domove/xn--b1arjbl.xn--90ae/public/js/cart.js`
*/
jQuery(function ($) {
'use strict';
let consents = localStorage.consents ? JSON.parse(localStorage.getItem('consents')) : {};
// cart will go finally to order.items
let cart = localStorage.cart ? JSON.parse(localStorage.cart) : {};
let order = localStorage.order ? JSON.parse(localStorage.order) : {};
const deliverers = {
econt: 'Еконт',
email: 'Е-Поща'
};
const currency = {
BGN: 'лв.',
EUR: 'евро'
};
const cart_widget_template = `
<div id="cart_widget" class="card text-center">
<button class="button primary outline icon cart" title="Показване/Скриване на поръчката"
id="show_cart"><span class="order_total"></span><img src="/img/cart.svg" width="32" height="32" /></button>
<h3 style="display:none">Поръчка</h3>
<table style="display:none">
<thead><tr><th>Изделие</th><th>Ед. цена</th><th>Бр.</th><th><!-- action --></th></tr></thead>
<tbody><!-- Here will be the order items --></tbody>
<tfoot>
<tr><th>Тегло (кг.)</th><th class="order_weight"></th><th></th><th></th></tr>
<tr><th>Общо (лв.)</th><th class="order_total"></th>
<th>
<button class="button primary outline icon cart pull-left"
title="Отказ от поръчката" id="cancel_order"><img
src="/img/cart-off.svg" width="32" /></button>
</th>
<th>
<button class="button primary primary icon cart"
title="Купувам (Доставка с Еконт)" id="econt_order"><img
src="/img/cart-check.svg" width="32" /></button><!--
<button class="button primary icon cart"
title="Купувам" id="email_order"><img
src="/img/cart-check.svg" width="32" /></button> -->
</th>
</tr>
<tr>
<th colspan="4"><a id="delivery_prices_url" title="Вижте подробности на страницата за цени на доставките."
href="">Ние поемаме част от доставката!</a></th>
</tfoot>
</table>
</div>
`;
const email_order_template = `
<div id="email_order_layer" style="display:none">
<form id="email_order_form" method="POST" action="/api/poruchki" class="container">
<fieldset class="card">
<legend data-description="Повечето от полетата, които попълвате тук, съдържат лични данни. Нужно е да ги предоставите, за да поръчате."
>Поръчка</legend>
<button class="button outline icon cart pull-right" title="Скриване на формуляра"
id="hide_email_order"><span class="order_total"></span><img src="/img/arrow-collapse-all.svg" width="32" /></button>
<button class="button outline icon cart pull-right" title="Пояснения"
id="help_email_order"><img src="/css/malka/help-circle-outline.svg" width="32" /></button>
<label for="recipient_names">Получател</label>
<input type="text" name="recipient_names" placeholder="Иванка Петканова" required="required" maxlength="100"
title="Собствено и родово име или име на фирма, получател."/>
<label for="email">E-поща</label>
<input type="email" name="email" placeholder="ivanka@primer.com" required="required" maxlength="100"
title="Адрес, на който ще получите уведомление, когато предадем пратката на доставчика. При закупуване на невеществени изделия (електронни книги, софтуер…) на този адрес ще получите връзка за изтегляне на файла."/>
<label for="phone">Телефон</label>
<input type="tel" name="phone" placeholder="0891234567" required="required" maxlength="20"
title="Телефонен номер, на който ще бъдете известени от доставчика, когато пратката ви пристигне."/>
<label for="deliverer">Предпочитан доставчик</label>
<select name="deliverer" title="Изберете кой от доставчиците, с които работим, предпочитате. При закупуване на невеществени изделия, ще изпратим връзка за изтегляне и банкова сметка, на която да преведете сумата по поръчката.">
<option value="email">Е-поща (за електронни издания и софтуер)</option>
<option value="econt">Еконт</option>
</select>
<label for="address">Адрес за получаване</label>
<input type="text" name="address" placeholder="п.к. 3210 с. Горно Нанадолнище, ул. „Цветуша“ №123" maxlength="155"
title="Вашият точен адрес за получаване на пратката или адрес на офис на избрания доставчик. При закупуване на невеществени изделия и услуги това поле не се попълва." />
<label for="notes">Допълнителни бележки</label>
<textarea name="notes" rows="2" maxlength="255"
title="Ако желаете да добавите някакви подробности и уточнения, въведете ги в това поле."></textarea>
<input type="hidden" name="items" value="{}"/>
<footer class="is-right" style="margin-top:1rem">
<button type="reset" class="secondary outline button icon-only"
class="reset_order_form" title="изчистване"><img src="/css/malka/card-bulleted-off-outline.svg" width="32" /></button>
<button class="primary button icon-only" type="submit"
title="Поръчвам"><img src="/img/cart-check.svg" width="32" /></button>
</footer>
</fieldset>
</form>
</div>
`;
const econt_order_template = `
<div id="econt_order_layer" style="display:none">
<div class="container card">
<form id="econt_order_form" method="POST" action="/api/poruchki">
<button class="button outline icon cart pull-right" title="Скриване на формуляра"
id="hide_econt_order"><img
src="/img/arrow-collapse-all.svg" width="32" /></button>
<h4><img src="/img/econt.svg" width="24" />Доставка с Еконт</h4>
<!-- В това поле ще се запази уникален индентификатор на адреса за доставка.
Попълва се от JavaScript функцията която 'слуша' съобщенията от формата за
доставка -->
<input type="hidden" name="customerInfo[id]">
<input type="hidden" name="cod" value="Да" readonly="" />
<table id="econt_order_items">
<cation class="text-center">Изделия</caption>
<tbody></tbody>
<tfoot></tfoot>
</table>
</form>
<!-- ФОРМА ЗА ДОСТАВКА -->
<fieldset>
<legend>Данни за доставка</legend>
<p>Полетата, които попълвате тук,
съдържат лични данни. Предоставяте ги на Еконт Експрес ООД единствено за извършване на доставката.</p>
<iframe id="econt_shipment" src=""></iframe>
</fieldset>
</div>
</div>
`;
const gdpr_consent_template = `
<div id="gdpr_consent" class="col" style="font-size:medium;font-family:sans-serif">
Ние не ви следим. Доставчикът ни ползва бисквитки, когато поръчвате.
Продължавайки се съгласявате с <a class="gdpr_consent_url text-success" href="">„Условията за ползване"</a>
на <a id="consents_ihost" href="/" class="text-success"></a>.
</div>
`;
const last_order_template = `
<div id="last_order_layer" style="display:none">
<div class="container card">
<button class="button outline icon cart pull-right hide_last_order"
title="Скриване"><span class="order_total"></span><img src="/img/arrow-collapse-all.svg" width="32" /></button>
<p>Вашата поръчка е приета. Ще бъдете уведомени своевременно от превозвача, когато вашата пратка пристигне.</p>
<section id="last_order_table">
<div class="row"><div class="col">Поръчка №:</div><div class="col" id="deliverer_id"></div></div>
<div class="row"><div class="col">Получател:</div><div class="col" id="name"></div></div>
<div class="row"><div class="col">E-поща:</div><div class="col" id="email"></div></div>
<div class="row"><div class="col">Телефон:</div><div class="col" id="phone"></div></div>
<div class="row"><div class="col">Доставчик:</div><div class="col" id="deliverer"></div></div>
<div class="row"><div class="col">Адрес за получаване:</div>
<div class="col"><span id="city_name"></span><span id="address"></span></div>
</div>
<div class="row"><div class="col">Товарителница:</div><div class="col" id="way_bill_id"></div></div>
<!-- <div class="row"><div class="col">Допълнителни бележки:</div><div class="col" id="notes"></div></div> -->
<table id="last_order_items">
<cation class="text-center">Изделия</caption>
<tbody></tbody>
<tfoot></tfoot>
</table>
<footer class="is-center">
<button class="button primary hide_last_order">Добре</button>
</footer>
</div>
</div>
`;
show_gdpr_consent();
show_cart();
/* In a regular page we present a product(book, software package,
* whatever). On the page there is one or more buttons(one per product)
* (.add-to-cart) in which data-attributes are stored all the properties
* of the product. Clicking on the button adds the product to the card.
* */
$('.add-to-cart').click(add_to_cart);
function add_to_cart() {
let product = $(this).data();
let product_id = '_' + product.sku;
if (product_id in cart) {
++cart[product_id].quantity;
} else {
cart[product_id] = {
sku: product.sku,
title: product.title,
quantity: 1,
weight: product.weight,
price: product.price
};
}
// display the cart
show_cart();
// expand it
$('#show_cart').trigger('click');
}
function cancel_order() {
console.log('#cancel_order');
localStorage.removeItem('cart');
cart = {};
$('#cart_widget').remove();
}
function show_cart() {
show_last_order_button();
// there is nothing to show? return.
if (!Object.keys(cart).length) return;
// Store the changed cart!!!
localStorage.setItem('cart', JSON.stringify(cart));
let cart_widget = $('#cart_widget');
//if not yet in dom, create it
if (!cart_widget.length) {
$('body').append(cart_widget_template);
//populate the table>tbody with the contents of the cart
$(Object.keys(cart)).each(populate_order_table);
// make the cart button to toggle the visibility of the products table
$('#show_cart').click(toggle_order_table_visibility);
// append the email_order_form
// if (!$('#email_order_layer').length)
// $('body').append(email_order_template);
// append the econt_order_template
if (!$('#econt_order_layer').length)
$('body').append(econt_order_template);
$('#delivery_prices_url').attr('href', consents.delivery_prices_url);
}
//else update it
else {
repopulate_order_table();
}
// calculate the sums of the order from the items and rules in the cart
let sum = 0;
let weight = 0;
Object.keys(cart).forEach((key) => {
weight += cart[key].weight * cart[key].quantity;
sum += cart[key].price * cart[key].quantity;
});
/*
const delivery_price = 4.00; // in he shop currency. Todo: make this a configuration value of the shop.
const free_delivery_sum = 35;
// VAT and delivery_price are included in the price if total sum < free_delivery_sum
if (sum > free_delivery_sum)
$('th.delivery_price').text(0.00.toFixed(2));
else
$('th.delivery_price').text(delivery_price.toFixed(2));
*/
$('.order_total').html(sum.toFixed(2));
$('.order_weight').html(weight.toFixed(3));
$('#cancel_order').click(cancel_order);
$('#econt_order').click(show_econt_order);
// Display an order form and handle data collection from the user
//$('#email_order').click(show_email_order);
} // end function show_cart()
function populate_order_table() {
let this_id = this;
let ow_jq = '#cart_widget>table>tbody';
$(ow_jq).append(`
<tr id="${this_id}">
<td title="${cart[this_id].title}">${cart[this].title}</td>
<td>${cart[this_id].price}</td>
<td>${cart[this_id].quantity} бр.</td>
<td>
<img class="outline minus" title="Премахване на един брой" src="/img/cart-minus.svg" width="32" />
<img class="outline plus" title="Добавяне на един брой" src="/img/cart-plus.svg" width="32" />
<img class="outline remove" title="Без това изделие" src="/img/cart-remove.svg" width="32" />
</td>
</tr>`);
//Add functionality to plus,minus and remove
$(`${ow_jq} tr#${this_id} .minus`).click(function () {
if (cart[this_id].quantity == 1) {
remove_item();
return;
}
--cart[this_id].quantity;
localStorage.removeItem('cart');
show_cart();
});
$(`${ow_jq} tr#${this_id} .plus`).click(function () {
++cart[this_id].quantity;
localStorage.removeItem('cart');
show_cart();
});
$(`${ow_jq} tr#${this_id} .remove`).click(remove_item);
function remove_item() {
if (Object.keys(cart).length == 1) {
$('#cancel_order').trigger('click');
return;
}
delete cart[this_id];
localStorage.removeItem('cart');
show_cart();
}
} // end function populate_order_table()
function repopulate_order_table() {
$('#cart_widget>table>tbody').empty();
$(Object.keys(cart)).each(populate_order_table);
}
function toggle_order_table_visibility() {
let order_button_icon = $('#cart_widget #show_cart>img');
if (order_button_icon.attr('src').match(/cart/))
order_button_icon.attr('src', '/img/arrow-collapse-all.svg');
else
order_button_icon.attr('src', '/img/cart.svg');
$('#cart_widget>h3,#cart_widget>table').toggle();
}
function show_econt_order() {
$('#econt_order_layer').show();
let hide = $('#hide_econt_order');
hide.off('click');
hide.click(function (e) {
$('#econt_order_layer').hide();
e.preventDefault();
});
get_set_shop_data();
handle_econt_order_form();
} // end function show_econt_order()
function get_set_shop_data() {
let shop_data = localStorage.shop_data ? JSON.parse(localStorage.shop_data) : {
retrieved: 1
};
let now = parseInt(Date.now() / 1000); // get seconds since 1970
let eco_shipment = $('#econt_shipment');
let items = [];
Object.keys(cart).forEach((key) => items.push(cart[key]));
inline_order_items('#econt_order_items', items);
order.items = items;
// Get fresh shop_data not older than 1 hour and do not get shop data
// on each display of order checkout, because shop_data may change on the server.
if ((now - shop_data.retrieved) < 3600 * 1) {
if (!eco_shipment.prop('src').match(new RegExp(shop_data.shop_id))) {
eco_shipment.prop('src', prepare_shipment_url(shop_data));
}
} else {
$.get('/api/shop').done(function (data) {
data.retrieved = now;
localStorage.setItem('shop_data', JSON.stringify(data));
eco_shipment.prop('src', prepare_shipment_url(data));
}).fail(function (jqXHR, textStatus, errorThrown) {
console.log(jqXHR, textStatus, errorThrown);
alert(
'Състояние:' + textStatus +
'ГРЕШКА: \n' + errorThrown);
});
}
}
// https://www.econt.com/developers/44-implementirane-na-dostavi-s-ekont-v-elektronniya-vi-magazin.html
function prepare_shipment_url(shop) {
const country_code = 1033; // Bulgaria
let shipment = {
// Get user data from localStorage from previous time if it exists
customer_company: order.name,
customer_name: order.face,
customer_phone: order.phone,
customer_email: order.email,
customer_country: (order.id_country ? order.id_country : country_code),
customer_post_code: order.post_code,
customer_office_code: order.office_code,
customer_address: order.address,
id_shop: shop.shop_id,
order_currency: shop.shop_currency,
order_total: parseFloat($('#cart_widget table>tfoot .order_total').text()),
order_weight: parseFloat($('#cart_widget table>tfoot .order_weight').text()),
customer_address: order.address,
ignore_history: 0,
confirm_txt: 'Потвърждавам'
};
let params = $.param(shipment, true);
return `${shop.shippment_calc_url}?${params}`;
}
//2.3. JavaScript функця, която получава резултата от формата за доставка:
function handle_econt_order_form() {
window.addEventListener('message', handle_econt_order_confirm, false);
}
/* Added as EventListener to econt iframe in which is placed the "Deliver
with Econt" confirmation form */
function handle_econt_order_confirm(message) {
// Елемент от кода, където е указано дали стоката ще се заплаща с НП или не
let codInput = document.getElementsByName('cod')[0];
//Елемент от формата в който ще се постави уникалното ID на доставката
let customerInfoIdInput = document.getElementsByName('customerInfo[id]')[0];
//Формата в която се съдържат данните по поръчката в магазина и същата трябва да се подаде
let econt_order_form = document.getElementById('econt_order_form');
// добавяне на функция, която 'слуша' данни връщани от формите за доставка
// Данни връщани от формата за доставка:
// id: уникално ID на адреса. Това поле трябва да бъде поставено в скритото customerInfo[id]
// id_country: ID на държавата
// zip: зип код на населеното място
// post_code: пощенски код на населеното място
// city_name: населено място
// office_code: код на офиса на Еконт ако бъде избран такъв
// address: адрес
// name: име / фирма
// face: лице
// phone: телефон
// email: имейл
// shipping_price: цена на пратката без НП
// shipping_price_cod: цена на пратката с НП
// shipping_price_currency: валута на калкулираната цена
// shipment_error: поясняващ текст ако е възникнала грешка
let data = message['data'];
if (data.office_code) {
delete data.num;
data.address = data.office_name;
delete data.street;
delete data.other;
delete data.quarter;
// todo: get the office address somehow (via some API call) to show it to the user
// data.address = $(`option[value=${data.office_code}]`, document.getElementById('econt_shipment').contentWindow.document).text();
}
order = {
...data,
items: order.items,
deliverer: 'econt',
sum: parseFloat($('#cart_widget table>tfoot .order_total').text()),
weight: parseFloat($('#cart_widget table>tfoot .order_weight').text())
};
//localStorage.setItem('order', JSON.stringify(order));
// възможно е да възникнат грешки при неправилно конфигурирани настройки на електронния магазин, които пречат за калкулацията
if (data['shipment_error'] && data['shipment_error'] !== '') {
alert('Възникна грешка при изчисляване на стройноста на пратката.\n' +
data['shipment_error'] +
'\nМолим пишете ни на otzivi@studio-berov.eu,' +
' като приложите грешката към писмото и ние ще направим всичко възможно да я отстраним.');
}
// формата за изчисляване връща цена с НП и такава без
// спрямо избора на клиента в "Заплащане чрез НП" показваме правилната цена
let shippmentPrice;
if (codInput) shippmentPrice = parseFloat(data['shipping_price_cod']);
else shippmentPrice = parseFloat(data['shipping_price']);
$('#econt_order_items tfoot>tr:last-child>td').replaceWith(`${shippmentPrice.toFixed(2)} ${currency[data['shipping_price_currency']]}`);
let confirmMessage =
`Куриерската ви услуга е на стройност ${shippmentPrice} ${currency[data['shipping_price_currency']]}
Наложеният платеж е на стойност ${order.sum.toFixed(2)} ${currency[data['shipping_price_currency']]}
Общо: ${(shippmentPrice + order.sum).toFixed(2)} ${currency[data['shipping_price_currency']]}
Потвърждавате ли покупката?`;
if (confirm(confirmMessage)) {
//customerInfoIdInput.value = data['id'];
// confirmForm.submit();
// Prevent this event to fire twice or more times after first confirmation
window.removeEventListener("message", handle_econt_order_confirm, false);
place_econt_order(econt_order_form)
}
}
/* Inline order items into econt_order_template or last_order_template */
function inline_order_items(table_id, items) {
let tbody = $(`${table_id} tbody`);
let tfoot = $(`${table_id} tfoot`);
tbody.empty();
tfoot.empty();
let sum = 0;
let weight = 0;
for (const it of items) {
let item_sum = it.price * it.quantity;
sum += item_sum;
weight += it.weight * it.quantity;
tbody.append(`
<tr>
<th title="${it.title}">${it.title}</th><td>${it.price}</td><td>${it.quantity} бр.</td><td>${item_sum.toFixed(2)}</td>
</tr>
`);
}
tfoot.append(`<tr><th colspan="3">Общо</th><td>${sum.toFixed(2)} лв.</td></tr>`);
tfoot.append(`<tr><th colspan="3">Тегло</th><td>${weight.toFixed(3)} кг.</td></tr>`);
tfoot.append(`<tr><th colspan="3">Доставка</th><td></td></tr>`);
}
/*
* 3. Генериране/редактиране на поръчка към "Достави с Еконт" http://delivery.econt.com/services/
* Услугата може да бъде извикана в следните случаи:
* - При завършване на поръчката от клиента;
* - При редактиране на поръчката от търговеца в рамките на административния си панел;
*/
// Приключване на поръчката.
function place_econt_order(form) {
let req = $.ajax({
url: $(form).prop('action'),
method: 'POST',
data: JSON.stringify({
Poruchka: order
}),
dataType: 'json'
});
// Prevent further firing of econt iframe eventListener
$('#econt_order_layer').remove();
req.done(function (data) {
// store the order for showing later too
localStorage.setItem('order', JSON.stringify(data));
order = data; // last order
delete_cart();
show_last_order(data);
});
req.fail(function (jqXHR, textStatus, errorThrown) {
let json = JSON.parse(jqXHR.responseText);
alert(`
Състояние: ${jqXHR.status} ${errorThrown}
ГРЕШКА: ${json.errors[0].path}
${json.errors[0].message}
Нещо се обърка на сървъра.
`);
});
} // end place_econt_order(form)
/**
* Displays the just made or last order to the user
*/
function show_last_order(o_d) {
let now = parseInt(Date.now() / 1000); // get seconds since 1970
// Get fresh o_d not older than 2 hours, because order_data may change
// on the server and do not get order data on each display.
if ((now - o_d.tstamp) > 3600 * 2) {
let req = $.ajax({
url: `/api/poruchka/${o_d.deliverer}/${o_d.deliverer_id}`,
method: 'PUT',
data: {
id: o_d.id
},
dataType: 'json'
});
req.done(function (data) {
localStorage.setItem('order', JSON.stringify(data));
o_d = order = data;
});
req.fail(function (jqXHR, textStatus, errorThrown) {
console.log(jqXHR, textStatus, errorThrown);
alert(
'Състояние:' + textStatus +
'ГРЕШКА: \n' + errorThrown);
});
}
let lol = '#last_order_layer';
$(lol).remove();
$('body').append(last_order_template);
let delivery = ['deliverer_id', 'name', 'email', 'phone', 'deliverer', 'office_name', 'address'];
let lot = '#last_order_table';
for (const k of delivery) {
if (o_d[k])
k == 'deliverer' ? $(`${lot} #${k}`).text(deliverers[o_d[k]]) : $(`${lot} #${k}`).text(o_d[k]);
}
if (o_d.way_bill_id)
$(`${lot} #way_bill_id`)
.html(`<a target="_blank" href="https://www.econt.com/services/track-shipment/${o_d.way_bill_id}">${o_d.way_bill_id}</a>`);
inline_order_items('#last_order_items', o_d.items);
$('#last_order_items tfoot tr:last-child td:last-child')
.text(`${o_d.shipping_price_cod} ${currency[o_d.shipping_price_currency]}`);
/* if (o_d.email)
$(`${lol} p:first-child`).append(`На електронната ви поща ще изпратим номера на
товарителницата, с който можете да проследите пратката.`); */
$(lol).show();
$('.hide_last_order').click(function (e) {
$(lol).hide();
e.preventDefault();
});
} // end function show_last_order
//Disabled for now. Will be shown when the order implementation is ready.
function show_last_order_button() {
//return;
if (!order.deliverer) return;
if ($('#last_order_button').length) return;
$('nav.nav-center').append(
` <button class="button primary outline icon sharer" title="Вашата последна поръчка"
id="last_order_button"><img src="/img/cart-arrow-right.svg" width="24"></button>`);
$('#last_order_button').click(function () {
show_last_order(order)
});
}
/**
* Delete the just made by the user order.
*/
function delete_cart() {
console.log('#delete_made_order');
localStorage.removeItem('cart');
cart = {};
$('#cart_widget').remove();
}
/**
* Show in the footer a line stating that we do not use cookies except for...
* */
function show_gdpr_consent() {
if (consents.visited === true) return;
if (consents.gdpr_url === undefined)
$.get('/api/consents').done(function (data) {
set_gdpr_consent(data);
}).fail(function (jqXHR, textStatus, errorThrown) {
console.log(jqXHR, textStatus, errorThrown);
});
else
set_gdpr_consent(consents);
} // end show_gdpr_consent()
function set_gdpr_consent(consents) {
// Do not show the message and store the consent if we are on the
// consents.gdpr_url page.
let footer = $('body>footer.is-fixed')
if (decodeURI(window.location.pathname) === consents.gdpr_url) {
consents.visited = true;
localStorage.setItem('consents', JSON.stringify(consents));
// A button, that gets the user to where he/she was.
// Note! It is important to not use the window.history object so
// the page loads fresh with no gdpr_consent message.
footer.html(`<a href="${document.referrer}"
title="Назад, откъдето дойдох."
class="col outline button text-success">Добре!</a>`);
return;
}
footer.html(gdpr_consent_template);
$('#consents_ihost').text(consents.ihost);
$('.gdpr_consent_url').attr('href', consents.gdpr_url);
localStorage.setItem('consents', JSON.stringify(consents));
} // end set_gdpr_consent(consents)
/* All code below relates to email order and is not used currently. */
/*************************************************/
function show_email_order() {
$('#email_order_layer').show();
let hide = $('#hide_email_order');
hide.off('click');
hide.click(function (e) {
$('#email_order_layer').hide();
e.preventDefault();
});
let help = $('#help_email_order');
help.off('click');
//Display help text for each field in the form.
let fo = '#email_order_form';
help.click(function (e) {
e.preventDefault();
let legend = $(`${fo} legend`);
let titles = `
<button class="button outline icon cart pull-right" title="Скриване на поясненията"
id="hide_order_help"><img src="/img/arrow-collapse-all.svg" width="32" /></button>
<h4>${legend.text()}</h4><p>${legend.data('description')}</p>`;
// take the help from label titles
$(`${fo} label`).each(function () {
let self = $(this);
let field_title = $(`${fo} [name=${self.prop('for')}]`).prop('title');
titles += `<h5>${self.html()}</h5><p>${field_title}</p>`;
});
// display the help
$('#email_order_layer').append(`<section id="order_help" class="card">${titles}</section>`);
// remove the help from the DOM
$('#hide_order_help').click(() => $('#order_help').remove());
});
let fields = $(`${fo} :input`);
// Populate each field (excluding order items) with previous data if there is such.
fields.each(function () {
if (order[$(this).prop('name')])
$(this).val(order[$(this).prop('name')]);
});
// Add events to each field to save the state (value) of the field so
// the next time the user will have the info ready and prefilled.
fields.change(function () {
order[$(this).prop('name')] = $(this).val();
localStorage.setItem('order', JSON.stringify(order));
});
$(fo).off('submit');
$(fo).submit(submit_order);
} // end function show_email_order()
function submit_order(ev) {
let fo = '#email_order_form';
let fields = $(`${fo} :input`);
// make sure all fields data goes to order
fields.each(function () {
if ($(this).prop('name') !== '')
order[$(this).prop('name')] = $(this).val();
});
order.items = [];
// Put the cart items as order items
Object.keys(cart).forEach((curr) => order.items.push(cart[curr]));
$.post($(fo).prop('action'), JSON.stringify(order), function (data, status) {
if (status === 'success') {
// store the order for showing later too
localStorage.setItem('last_order', JSON.stringify(data));
delete_cart();
$('#email_order_layer').hide();
show_last_order(data);
} else {
let response = JSON.stringify(data);
alert(`
Нещо се обърка на сървъра.
Опитайте да изпратите поръчката си на poruchki@studio-berov.eu.
Следва отговорът от сървъра. Молим изпратете, снимка на екрана си, за да ни
улесните в отстраняването на грешката.
${response}
`);
}
}, 'json');
ev.preventDefault();
} // end function submit_order
});
@@ img/arrow-collapse-all.svg
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M19.5,3.09L20.91,4.5L16.41,9H20V11H13V4H15V7.59L19.5,3.09M20.91,19.5L19.5,20.91L15,16.41V20H13V13H20V15H16.41L20.91,19.5M4.5,3.09L9,7.59V4H11V11H4V9H7.59L3.09,4.5L4.5,3.09M3.09,19.5L7.59,15H4V13H11V20H9V16.41L4.5,20.91L3.09,19.5Z" /></svg>
@@ img/cart-arrow-right.svg
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24">
<path fill="#fff" d="M9,20A2,2 0 0,1 7,22A2,2 0 0,1 5,20A2,2 0 0,1 7,18A2,2 0 0,1 9,20M17,18A2,2 0 0,0 15,20A2,2 0 0,0 17,22A2,2 0 0,0 19,20A2,2 0 0,0 17,18M7.2,14.63C7.19,14.67 7.19,14.71 7.2,14.75A0.25,0.25 0 0,0 7.45,15H19V17H7A2,2 0 0,1 5,15C5,14.65 5.07,14.31 5.24,14L6.6,11.59L3,4H1V2H4.27L5.21,4H20A1,1 0 0,1 21,5C21,5.17 20.95,5.34 20.88,5.5L17.3,12C16.94,12.62 16.27,13 15.55,13H8.1L7.2,14.63M9,9.5H13V11.5L16,8.5L13,5.5V7.5H9V9.5Z" />
</svg>
@@ img/cart.svg
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M17,18C15.89,18 15,18.89 15,20A2,2 0 0,0 17,22A2,2 0 0,0 19,20C19,18.89 18.1,18 17,18M1,2V4H3L6.6,11.59L5.24,14.04C5.09,14.32 5,14.65 5,15A2,2 0 0,0 7,17H19V15H7.42A0.25,0.25 0 0,1 7.17,14.75C7.17,14.7 7.18,14.66 7.2,14.63L8.1,13H15.55C16.3,13 16.96,12.58 17.3,11.97L20.88,5.5C20.95,5.34 21,5.17 21,5A1,1 0 0,0 20,4H5.21L4.27,2M7,18C5.89,18 5,18.89 5,20A2,2 0 0,0 7,22A2,2 0 0,0 9,20C9,18.89 8.1,18 7,18Z" /></svg>
@@ img/cart-check.svg
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="#fff" d="M9 20C9 21.11 8.11 22 7 22S5 21.11 5 20 5.9 18 7 18 9 18.9 9 20M17 18C15.9 18 15 18.9 15 20S15.9 22 17 22 19 21.11 19 20 18.11 18 17 18M7.17 14.75L7.2 14.63L8.1 13H15.55C16.3 13 16.96 12.59 17.3 11.97L21.16 4.96L19.42 4H19.41L18.31 6L15.55 11H8.53L8.4 10.73L6.16 6L5.21 4L4.27 2H1V4H3L6.6 11.59L5.25 14.04C5.09 14.32 5 14.65 5 15C5 16.11 5.9 17 7 17H19V15H7.42C7.29 15 7.17 14.89 7.17 14.75M18 2.76L16.59 1.34L11.75 6.18L9.16 3.59L7.75 5L11.75 9L18 2.76Z" />
</svg>
@@ img/cart-off.svg
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M22.73,22.73L1.27,1.27L0,2.54L4.39,6.93L6.6,11.59L5.25,14.04C5.09,14.32 5,14.65 5,15A2,2 0 0,0 7,17H14.46L15.84,18.38C15.34,18.74 15,19.33 15,20A2,2 0 0,0 17,22C17.67,22 18.26,21.67 18.62,21.16L21.46,24L22.73,22.73M7.42,15A0.25,0.25 0 0,1 7.17,14.75L7.2,14.63L8.1,13H10.46L12.46,15H7.42M15.55,13C16.3,13 16.96,12.59 17.3,11.97L20.88,5.5C20.96,5.34 21,5.17 21,5A1,1 0 0,0 20,4H6.54L15.55,13M7,18A2,2 0 0,0 5,20A2,2 0 0,0 7,22A2,2 0 0,0 9,20A2,2 0 0,0 7,18Z" /></svg>
@@ img/cart-minus.svg
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M16,6V4H8V6M7,18A2,2 0 0,0 5,20A2,2 0 0,0 7,22A2,2 0 0,0 9,20A2,2 0 0,0 7,18M17,18A2,2 0 0,0 15,20A2,2 0 0,0 17,22A2,2 0 0,0 19,20A2,2 0 0,0 17,18M7.17,14.75L7.2,14.63L8.1,13H15.55C16.3,13 16.96,12.59 17.3,11.97L21.16,4.96L19.42,4H19.41L18.31,6L15.55,11H8.53L8.4,10.73L6.16,6L5.21,4L4.27,2H1V4H3L6.6,11.59L5.25,14.04C5.09,14.32 5,14.65 5,15A2,2 0 0,0 7,17H19V15H7.42C7.29,15 7.17,14.89 7.17,14.75Z" /></svg>
@@ img/cart-plus-white.svg
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="white" d="M11,9H13V6H16V4H13V1H11V4H8V6H11M7,18A2,2 0 0,0 5,20A2,2 0 0,0 7,22A2,2 0 0,0 9,20A2,2 0 0,0 7,18M17,18A2,2 0 0,0 15,20A2,2 0 0,0 17,22A2,2 0 0,0 19,20A2,2 0 0,0 17,18M7.17,14.75L7.2,14.63L8.1,13H15.55C16.3,13 16.96,12.59 17.3,11.97L21.16,4.96L19.42,4H19.41L18.31,6L15.55,11H8.53L8.4,10.73L6.16,6L5.21,4L4.27,2H1V4H3L6.6,11.59L5.25,14.04C5.09,14.32 5,14.65 5,15A2,2 0 0,0 7,17H19V15H7.42C7.29,15 7.17,14.89 7.17,14.75Z" /></svg>
@@ img/cart-plus.svg
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M11,9H13V6H16V4H13V1H11V4H8V6H11M7,18A2,2 0 0,0 5,20A2,2 0 0,0 7,22A2,2 0 0,0 9,20A2,2 0 0,0 7,18M17,18A2,2 0 0,0 15,20A2,2 0 0,0 17,22A2,2 0 0,0 19,20A2,2 0 0,0 17,18M7.17,14.75L7.2,14.63L8.1,13H15.55C16.3,13 16.96,12.59 17.3,11.97L21.16,4.96L19.42,4H19.41L18.31,6L15.55,11H8.53L8.4,10.73L6.16,6L5.21,4L4.27,2H1V4H3L6.6,11.59L5.25,14.04C5.09,14.32 5,14.65 5,15A2,2 0 0,0 7,17H19V15H7.42C7.29,15 7.17,14.89 7.17,14.75Z" /></svg>
@@ img/cart-remove.svg
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M14.12,8.53L12,6.41L9.88,8.54L8.46,7.12L10.59,5L8.47,2.88L9.88,1.47L12,3.59L14.12,1.46L15.54,2.88L13.41,5L15.53,7.12L14.12,8.53M7,18A2,2 0 0,1 9,20A2,2 0 0,1 7,22A2,2 0 0,1 5,20A2,2 0 0,1 7,18M17,18A2,2 0 0,1 19,20A2,2 0 0,1 17,22A2,2 0 0,1 15,20A2,2 0 0,1 17,18M7.17,14.75A0.25,0.25 0 0,0 7.42,15H19V17H7A2,2 0 0,1 5,15C5,14.65 5.09,14.32 5.25,14.04L6.6,11.59L3,4H1V2H4.27L5.21,4L6.16,6L8.4,10.73L8.53,11H15.55L18.31,6L19.41,4H19.42L21.16,4.96L17.3,11.97C16.96,12.59 16.3,13 15.55,13H8.1L7.2,14.63L7.17,14.75Z" /></svg>
@@ img/econt.svg
<svg version="1.1" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><path style="opacity:1;fill:#234182;fill-opacity:1;stroke-width:1.24709" d="M 6.1289062 -0.001953125 C 4.6048195 -0.017998455 3.2956311 1.2947208 3.125 3.0820312 L 1.5019531 20.080078 C 1.4422923 20.705007 1.5321941 21.30588 1.734375 21.841797 C 2.0990696 23.087443 3.2446653 23.992188 4.6113281 23.992188 L 17.267578 23.992188 C 18.929578 23.992188 20.267578 22.654187 20.267578 20.992188 C 20.267578 19.330188 18.929578 17.992188 17.267578 17.992188 L 7.7382812 17.992188 L 8.8828125 6 L 19.546875 6 C 21.208875 6 22.546875 4.662 22.546875 3 C 22.546875 1.338 21.208875 0 19.546875 0 L 6.484375 0 C 6.4201955 0 6.3580767 0.0058336146 6.2949219 0.009765625 C 6.2393916 0.0056858294 6.1839178 -0.001373973 6.1289062 -0.001953125 z M 15.328125 8.0390625 A 4 4 0 0 0 11.328125 12.039062 A 4 4 0 0 0 15.328125 16.039062 A 4 4 0 0 0 19.328125 12.039062 A 4 4 0 0 0 15.328125 8.0390625 z " /></svg>
@@ img/phone-classic-white.svg
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="#fff" d="M12,3C7.46,3 3.34,4.78 0.29,7.67C0.11,7.85 0,8.1 0,8.38C0,8.66 0.11,8.91 0.29,9.09L2.77,11.57C2.95,11.75 3.2,11.86 3.5,11.86C3.75,11.86 4,11.75 4.18,11.58C4.97,10.84 5.87,10.22 6.84,9.73C7.17,9.57 7.4,9.23 7.4,8.83V5.73C8.85,5.25 10.39,5 12,5C13.59,5 15.14,5.25 16.59,5.72V8.82C16.59,9.21 16.82,9.56 17.15,9.72C18.13,10.21 19,10.84 19.82,11.57C20,11.75 20.25,11.85 20.5,11.85C20.8,11.85 21.05,11.74 21.23,11.56L23.71,9.08C23.89,8.9 24,8.65 24,8.37C24,8.09 23.88,7.85 23.7,7.67C20.65,4.78 16.53,3 12,3M9,7V10C9,10 3,15 3,18V22H21V18C21,15 15,10 15,10V7H13V9H11V7H9M12,12A4,4 0 0,1 16,16A4,4 0 0,1 12,20A4,4 0 0,1 8,16A4,4 0 0,1 12,12M12,13.5A2.5,2.5 0 0,0 9.5,16A2.5,2.5 0 0,0 12,18.5A2.5,2.5 0 0,0 14.5,16A2.5,2.5 0 0,0 12,13.5Z" /></svg>
@@ partials/_footer_right.html.ep
<div class="pull-right social text-right">
<%
my $sharer_url = $canonical_path;
my $phone_url = app->config->{consents}{phone_url};
%>
<a class="button outline primary sharer" href="tel:<%== $phone_url %>"
title="<%== $phone_url %>"><img style="float: left;"
src="/img/phone-classic-white.svg" height="24"> <%== $phone_url %></a><a
class="button outline primary sharer" target="_blank"
href="https://www.facebook.com/share.php?u=<%= $sharer_url %>" rel="noopener"
aria-label="Споделяне във Facebook"
title="Споделяне във Facebook"><img src="/css/malka/facebook.svg"></a><a
class="button outline primary sharer" target="_blank"
href="https://www.linkedin.com/shareArticle?mini=true&url=<%= $sharer_url %>&title=<%= title %>"
aria-label="Споделяне в LinkedIn"
title="Споделяне в LinkedIn"><img src="/css/malka/linkedin.svg"></a><a
class="button outline primary sharer" target="_blank"
href="https://twitter.com/intent/tweet?url=<%= $sharer_url %>&via=@kberov&title=<%= title %>"
aria-label="Споделяне в Twitter"
title="Споделяне в Twitter"><img src="/css/malka/twitter.svg"></a><a
class="button outline primary sharer" target="_blank"
href="mailto:?subject=<%= title %>&body=<%= $sharer_url %>"
aria-label="Напишете писмо на приятел"
title="Напишете писмо"><img src="/css/malka/email-fast-outline.svg"></a><a
class="button outline primary sharer" target="_blank"
href="tg://msg_url?url=<%= $sharer_url %>&text=<%= title %>"
aria-label="Споделяне в Telegram"
title="Споделяне в Telegram"><img src="/css/malka/icons8-telegram-app.svg"></a>
</div>
@@ partials/_consents.html.ep
<%
# This template is practically the same as _writing.html.ep, but demosntrates a
# good example how plugins can create their own 'data_type's and use templates
# to display them on the site. Now if a celina has _consents as data_type,
# it will be rendered using this template. With this simple technique we open
# the opportunity for content provided by plugins to get seamlesly pluged
# anywhere in the site, by just chosing the corresponding data_type and
# providing template for rendering this data_type.
%>
<!-- _consents -->
<section class="<%= $celina->{data_type} %>">
%= t 'h' . $level => $celina->{title}
<p><a href="<%== $c->req->headers->referrer %>"
title="Към страницата, от която дойдох."
class="outline button sharer referrer">←</a></p>
%$celina->{body} .= include 'partials/_created_tstamp';
%==format_body($celina)
<p><a href="<%== $c->req->headers->referrer %>"
title="Към страницата, от която дойдох."
class="outline button sharer referrer">←</a></p>
</section>
<!-- end _consents -->
@@ partials/_kniga.html.ep
<%
# An example of using an existing data_type to extend its functionality. Note
# that this template will replace the default one from Themes::Malka. So it is
# maybe a better idea to create a new data_type and use a new corresponding
# template. See the next template as an example of this simple technique to
# plug anywhere in the site.
my $books = $c->products;
my $variants = $books->all({
columns => '*',
where => {alias => $celina->{alias}, p_type => $celina->{data_type}}
})->each(sub {
$_->{properties} = Mojo::JSON::from_json($_->{properties});
});
return unless @$variants;
# $c->debug(' $variants' => $variants);
%>
<!-- _book -->
<!-- <%= $domain->{templates} %> -->
<section class="<%= $celina->{data_type} %>">
%# Get the product variants for this page. These are rows from table products
%# with the same alias like the page alias and with p_type same like celina
%# data_type
%= t 'h' . $level => $celina->{title}
<div class="row">
<figure class="col-3 text-center">
<img title="<%= $variants->[0]{title} %>" src="<%= $variants->[0]{properties}{images}[0] %>">
<figcaption class="text-center">
%= $variants->first(sub {$_->{properties}{in_store}}) ? 'За покупка' : 'Изчерпана';
<br />
% for my $b(@$variants) {
% my $props = $b->{properties}; next unless $props->{in_store};
<a class="primary sharer button add-to-cart" href="#show_cart" title="<%= $props->{variant} %>"
data-sku="<%= $b->{sku} %>" data-title="<%= $b->{title} %>"
data-weight="<%= $props->{weight} %>" data-price="<%= $props->{price} %>"
><img src="<%= $props->{button_icon} %>"> <img src="/img/cart-plus-white.svg"></a>
% }
</figcaption>
</figure>
<table id="meta" class="col card">
<tbody>
<tr><th>Заглавие:</th><td><%= $variants->[0]{title} %></td></tr>
<tr><th>Автор:</th><td><%= $variants->[0]{properties}{author} %></td></tr>
% if($variants->[0]{properties}{translator}) {
<tr><th>Преводач:</th><td><%= $variants->[0]{properties}{translator} %></td></tr>
% }
% if($variants->[0]{properties}{series}) {
<tr><th>Поредица:</th><td><%= $variants->[0]{properties}{series} %></td></tr>
% }
<tr><th>Размери:</th><td><%= $variants->[0]{properties}{dimensions} %></td></tr>
% for my $b(@$variants) {
% my $props = $b->{properties};
<tr class="separator">
<th>Издание:</th><td><%= $props->{variant} %></td></tr>
<tr><th>ISBN:</th><td><%= $b->{sku} %></td></tr>
<tr><th>Тегло:</th><td><%= sprintf('%.3f', $props->{weight}) %> кг.</td></tr>
<tr class="price"><th>Цена:</th><td><%= $props->{price} . ' лв. за ' . $props->{variant} %></td></tr>
% }
<tr class="separator">
% if($variants->[0]{properties}{exerpts_url}) {
<th>Откъси:</th><td><a class="primary button sharer" title="Изтегляне на откъси" href="<%=
$variants->[0]{properties}{exerpts_url}
%>"><img src="/css/malka/file-pdf-box.svg"><img src="/css/malka/download.svg"></a></td></tr>
% }
<tr><th>Е-поща:</th><td><a class="primary button sharer" title="Заявка по е-поща"
href="mailto:poruchki@studio-berov.eu?subject=Заявка: <%=$variants->[0]{title}%>">
<img src="/css/malka/email-fast-outline.svg">
<img src="/css/malka/book-open-page-variant-outline.svg">
Заявка по е-поща
</a></td></tr>
</tbody>
</table>
</div><!-- end class="row" -->
% $celina->{body} .= include 'partials/_created_tstamp';
%== format_body($celina)
%# all celini which have books in them in the same page
% my $others = $books->others($celina);
% if(@$others) {
<h4>Други книги</h4>
<div class="row text-left">
<%
# $c->debug('other books: ' => $others);
foreach my $b(@{$others->shuffle}) {
my $props = $b->{properties};
%>
<div class="col card text-center">
<a href="<%= url_for(page_alias => $page->{alias}, paragraph_alias => $b->{alias}, lang => $b->{language}) %>"
title="<%= "$b->{title}, $props->{author}" %>">
<img src="<%=$props->{images}[0]%>" style="<%= $props->{in_store} ? '' : 'filter:opacity(0.2)' %>">
</a>
</div>
% } # end foreach
</div>
% } #end if @$others
</section>
@@ resources/data/prodan_migrations.sql
-- 202112310000 up
-- A list of products and services being sold
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sku VARCHAR(40) UNIQUE NOT NULL,
-- Lowercased and trimmed of \W characters unique identifier.
-- Must be the same as the alias of the celina in which it will be embedded.
alias VARCHAR(100) NOT NULL,
-- Must be the same as the title of the celina in which it will be embedded.
title VARCHAR(155) UNIQUE NOT NULL,
-- The product type: книга(book), картина(painting), ръкоделие(handmade)
-- software... etc. If the plugin provides a template for the same celina
-- data_type. The template will rpelace the default Slovo template for this data
-- type and will retrieve from this table the product with the same alias as the
-- celina alias. The content of the celina will go after the products content.
p_type VARCHAR(32) DEFAULT 'book',
-- the properties which are put in the data-* attributes
-- of an "Add to cart" button such as data-sku, data-price,
-- data-vat, data-vat_included, data-title, data-description, etc.
properties JSON NOT NULL DEFAULT '{}'
);
CREATE UNIQUE INDEX IF NOT EXISTS sku_alias ON products(sku, alias);
-- A list of orders for bying product by customers
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- name - own and family name
name VARCHAR(100) NOT NULL,
email VARCHAR(100),
phone VARCHAR(20) NOT NULL,
deliverer VARCHAR(100) NOT NULL,
-- id of the order, given by the deliverer
deliverer_id VARCHAR(40) NOT NULL,
city_name VARCHAR(55) NOT NULL,
-- Order with product items as it comes from the deliverer form dispayed to the enduser.
-- Each item has the properties of a product.
poruchka JSON NOT NULL,
-- Way bill structure generated at deliverer site
way_bill JSON DEFAULT '{}',
-- When this content was inserted
created_at INTEGER NOT NULL DEFAULT 0,
-- Last time the record was touched
tstamp INTEGER DEFAULT 0,
-- Id at the deliverer site, returned by their system after we created the
-- way-bill at their site.
way_bill_id VARCHAR(40) DEFAULT '',
executed INT(1) DEFAULT 0
);
CREATE UNIQUE INDEX IF NOT EXISTS deliverer_id ON orders(deliverer, deliverer_id);
-- A list of invoices for services and products, produced by different users
-- of this system.
CREATE TABLE IF NOT EXISTS invoices (
-- Internal ID for this invoice. For the visible id each user will have its
-- own incrementing counter, implemented outside the database
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- The visible and printable invoice ID - unique per user
user_invoice_id INTEGER NOT NULL,
-- User who created the invoice (owner).
user_id INTEGER NOT NULL,
-- Which order is this invoice for? NOTE: Items for the invoice are taken from the order.
order_id INTEGER NOT NULL UNIQUE REFERENCES orders(id),
-- Who modified this record the last time?
changed_by INTEGER REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS users_invoices_last_id (
-- ID of the user which created this invoice
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
-- Internal invoice ID of the last invoice, generated by this user
invoice_id INTEGER REFERENCES invoices(id) ON DELETE CASCADE,
PRIMARY KEY(user_id, invoice_id)
);
-- 202112310000 down
DROP TABLE IF EXISTS invoices;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS users_invoices_last_id;
-- 202202170000 up
INSERT OR IGNORE INTO celini
(alias, pid, page_id, user_id, group_id, data_type, data_format, created_at,
tstamp, title, description, keywords, body, box, language, published)
VALUES(
'условия',
(SELECT id from celini WHERE page_id=(
SELECT id FROM stranici WHERE alias='ѿносно' AND dom_id=0) AND data_type='title'),
(SELECT id FROM stranici WHERE alias='ѿносно'),
(SELECT user_id FROM stranici WHERE alias='ѿносно'),
(SELECT group_id FROM stranici WHERE alias='ѿносно'),
'_gdpr_consent',
'html',
1645051275,
1645051275,
'Условия за ползване на Слово.бг',
'Щом ползвате Слово.бг, вие се съгласявате със следните условия.',
'условия,позване,права,ОРДЗ,GDPR',
'<p>Щом ползвате Слово.бг, вие се съгласявате със следните условия.</p>',
'main', 'bg', 2
);
-- 202202170000 down
DELETE from celini WHERE alias='условия'
AND pid=(SELECT id from celini WHERE page_id=(
SELECT id FROM stranici WHERE alias='ѿносно' AND dom_id=0) AND data_type='title');
-- 202203030000 up
INSERT OR IGNORE INTO celini
(alias, pid, page_id, user_id, group_id, data_type, data_format, created_at,
tstamp, title, description, keywords, body, box, language, published)
VALUES(
'цени-доставка',
(SELECT id from celini WHERE page_id=(
SELECT id FROM stranici WHERE alias='ѿносно' AND dom_id=0) AND data_type='title'),
(SELECT id FROM stranici WHERE alias='ѿносно'),
(SELECT user_id FROM stranici WHERE alias='ѿносно'),
(SELECT group_id FROM stranici WHERE alias='ѿносно'),
'_consents',
'html',
1646317805,
1646317805,
'Цени за доставка',
'Ние поемаме тридесет на сто (30%) от доставката…',
'условия,доставка,цени,поръчки',
'<p>Ние поемаме тридесет на сто (30%) от доставката, а при по-големи поръчки поемаме цялата доставка.</p>',
'main', 'bg', 2
);
UPDATE celini set data_type='_consents' where alias='условия'
AND pid = (SELECT id from celini WHERE page_id=(
SELECT id FROM stranici WHERE alias='ѿносно' AND dom_id=0)
AND data_type='title');
-- 202203030000 down
DELETE from celini WHERE alias='цени-доставка'
AND pid=(SELECT id from celini WHERE page_id=(
SELECT id FROM stranici WHERE alias='ѿносно' AND dom_id=0) AND data_type='title');