Merge branch 'master' of http://git.osufx.com/Gr1/husstanden
This commit is contained in:
commit
0550c9cc19
17
BABEL_USAGE.md
Normal file
17
BABEL_USAGE.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Babel usage
|
||||
Babel will scan and detect sections of code that uses `gettext` or `_` in HTML templates and python code. It takes the constant string passed into its function and makes a translation template which can then be used to build individual translations.
|
||||
|
||||
## Usage
|
||||
If you are compiling and building a template from scratch
|
||||
```
|
||||
$ pybabel extract -F babel.cfg -o messages.pot ./
|
||||
$ pybabel update -i messages.pot -d translations
|
||||
```
|
||||
|
||||
Make translations in the outputted file then compile the translation with
|
||||
```
|
||||
$ pybabel compile -d translations
|
||||
```
|
||||
|
||||
Make sure the `messages.po` file is in the right language directory.
|
||||
Example path: `translations/nb/LC_MESSAGES/messages.po`
|
|
@ -9,7 +9,11 @@ Install the required dependencies with pip
|
|||
```
|
||||
$ pip install -r requirements.txt
|
||||
```
|
||||
then, run the web server
|
||||
Make sure you compile the babel languages
|
||||
```
|
||||
$ pybabel compile -d translations
|
||||
```
|
||||
Lastly, run the web server
|
||||
```
|
||||
$ python3 main.py
|
||||
```
|
||||
|
|
3
babel.cfg
Normal file
3
babel.cfg
Normal file
|
@ -0,0 +1,3 @@
|
|||
[python: **.py]
|
||||
[jinja2: **/templates/**.html]
|
||||
extensions=jinja2.ext.autoescape,jinja2.ext.with_
|
|
@ -11,5 +11,12 @@
|
|||
"auto_pull_and_restart": false,
|
||||
"webhook_endpoint": "/api/git_commit",
|
||||
"secret": "iOnlyHavePullAccess"
|
||||
},
|
||||
"mysql": {
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"user": "husstanden",
|
||||
"passwd": "",
|
||||
"db": "db_husstanden"
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
from flask import render_template
|
||||
from objects import glob
|
||||
|
||||
"""
|
||||
@glob.app.template_filter('load_module')
|
||||
def load_module(module_name):
|
||||
return render_template("modules/%s.html" % module_name)
|
||||
"""
|
122
forms/login.py
Normal file
122
forms/login.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
from wtforms import Form, BooleanField, StringField, PasswordField, TextAreaField, validators
|
||||
from wtforms.fields.html5 import DateField, DecimalField, IntegerField, EmailField
|
||||
from wtforms.widgets import TextArea
|
||||
from flask_login import UserMixin
|
||||
from flask_babel import gettext as _
|
||||
|
||||
from objects import glob
|
||||
|
||||
FORM_RENDER_KW = {
|
||||
"class_": "form-control"
|
||||
}
|
||||
|
||||
class BillForm(Form):
|
||||
payment_to = StringField(_("Payment to"), [validators.DataRequired()], render_kw = FORM_RENDER_KW)
|
||||
description = TextAreaField(_("Description"), render_kw = {
|
||||
"cols": 55,
|
||||
"rows": 8,
|
||||
**FORM_RENDER_KW
|
||||
})
|
||||
sum = DecimalField(_("Sum"), render_kw = FORM_RENDER_KW)
|
||||
kid = IntegerField(_("KID"), render_kw = FORM_RENDER_KW)
|
||||
date_due = DateField(_("Date due"), render_kw = FORM_RENDER_KW)
|
||||
|
||||
class WarrantyForm(Form):
|
||||
item = StringField(_("Item"), [validators.DataRequired()], render_kw = FORM_RENDER_KW)
|
||||
date_from = DateField(_("Date of purchase"), render_kw = FORM_RENDER_KW)
|
||||
date_to = DateField(_("Warranty duration"), render_kw = FORM_RENDER_KW)
|
||||
sum = DecimalField(_("Sum"), render_kw = FORM_RENDER_KW)
|
||||
|
||||
class ServiceForm(Form):
|
||||
name = StringField(_("Name"), [validators.DataRequired()], render_kw = FORM_RENDER_KW)
|
||||
type = StringField(_("Type"), [validators.DataRequired()], render_kw = FORM_RENDER_KW)
|
||||
contact = StringField(_("Contact"), render_kw = FORM_RENDER_KW)
|
||||
phone = IntegerField(_("Phone"), render_kw = FORM_RENDER_KW)
|
||||
website = StringField(_("Website"), render_kw = FORM_RENDER_KW)
|
||||
|
||||
class LoginForm(Form):
|
||||
email = EmailField(_("Email"), [
|
||||
validators.DataRequired(),
|
||||
validators.Length(min=6, max=254)
|
||||
],
|
||||
render_kw = FORM_RENDER_KW)
|
||||
|
||||
password = PasswordField(_("Password"), [
|
||||
validators.DataRequired(),
|
||||
validators.Length(min=4, max=127)
|
||||
],
|
||||
render_kw = FORM_RENDER_KW)
|
||||
|
||||
class RegisterForm(Form):
|
||||
email = EmailField(_("Email"), [
|
||||
validators.DataRequired(),
|
||||
validators.Length(min=6, max=254)
|
||||
],
|
||||
render_kw = FORM_RENDER_KW)
|
||||
|
||||
password = PasswordField(_("Password"), [
|
||||
validators.DataRequired(),
|
||||
validators.Length(min=4, max=127),
|
||||
validators.EqualTo("confirm_password", message = _("Passwords must match"))
|
||||
],
|
||||
render_kw = FORM_RENDER_KW)
|
||||
confirm_password = PasswordField(_("Repeat Password"), render_kw = FORM_RENDER_KW)
|
||||
|
||||
firstname = StringField(_("Firstname"), [
|
||||
validators.DataRequired(),
|
||||
validators.Length(min=2, max=30)
|
||||
],
|
||||
render_kw = FORM_RENDER_KW)
|
||||
|
||||
surname = StringField(_("Surname"), [
|
||||
validators.DataRequired(),
|
||||
validators.Length(min=2, max=30)
|
||||
],
|
||||
render_kw = FORM_RENDER_KW)
|
||||
|
||||
accept_tos = BooleanField(_("I accept the TOS"), [validators.DataRequired()])
|
||||
|
||||
class User(UserMixin):
|
||||
id = -1
|
||||
email = ""
|
||||
password = ""
|
||||
firstname = ""
|
||||
surname = ""
|
||||
def __init__(self, login):
|
||||
self.fetch_from_db(login)
|
||||
|
||||
def fetch_from_db(self, login):
|
||||
conn = glob.get_sql_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("""
|
||||
SELECT *
|
||||
FROM Bruker
|
||||
WHERE Epost = %s
|
||||
LIMIT 1;
|
||||
""", (login[0],))
|
||||
|
||||
user = cur.fetchone()
|
||||
|
||||
cur.close()
|
||||
|
||||
if user is None:
|
||||
raise Exception(_("Invalid login"))
|
||||
|
||||
if not glob.check_password(login[1], user[2]):
|
||||
raise Exception(_("Incorrect password"))
|
||||
|
||||
self.id, self.email, self.password, self.firstname, self.surname = user
|
||||
|
||||
def register_account(email, password, firstname, surname):
|
||||
conn = glob.get_sql_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO
|
||||
Bruker (Epost, Passord, Fornavn, Etternavn)
|
||||
VALUES (%s, %s, %s, %s);
|
||||
""", (email, glob.hash_password(password), firstname, surname))
|
||||
|
||||
conn.commit()
|
||||
cur.close()
|
2
init.sql
2
init.sql
|
@ -5101,7 +5101,7 @@ CREATE TABLE Husrelasjoner
|
|||
(
|
||||
BrukerID INT(11),
|
||||
HusID INT(11),
|
||||
Privleges INT(11),
|
||||
Privileges INT(11),
|
||||
PRIMARY KEY (BrukerID, HusID),
|
||||
CONSTRAINT FOREIGN KEY (BrukerID) REFERENCES Bruker(BrukerID),
|
||||
CONSTRAINT FOREIGN KEY (HusID) REFERENCES Husstand(HusID)
|
||||
|
|
20
localizer.py
Normal file
20
localizer.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from flask import g, request, session
|
||||
from flask_babel import Babel
|
||||
|
||||
from objects import glob
|
||||
|
||||
babel = Babel(glob.app)
|
||||
|
||||
LANGUAGES = {
|
||||
"en": "English",
|
||||
"no": "Norwegian"
|
||||
}
|
||||
|
||||
@babel.localeselector
|
||||
def get_locale():
|
||||
# force session lang to be set
|
||||
session["lang"] = session.get("lang", "en")
|
||||
|
||||
if request.args.get("lang"):
|
||||
session["lang"] = request.args.get("lang") if request.args.get("lang") in LANGUAGES.keys() else "en"
|
||||
return session.get("lang", "en")
|
4
main.py
4
main.py
|
@ -3,9 +3,11 @@ from flask import Flask, url_for, request
|
|||
from objects import glob # Global sharing of python objects in a manageable way
|
||||
|
||||
glob.app = Flask(__name__)
|
||||
glob.app.secret_key = "E2FGrJXLtOxPh70Q"
|
||||
|
||||
import localizer # Initialize localization (Babel)
|
||||
import routes # All flask app routes
|
||||
import filters # All flask app filters
|
||||
# import filters # All flask app filters
|
||||
|
||||
if glob.config["git"]["auto_pull_and_restart"]: # Only used on the VPS (Do not enable in config)
|
||||
@glob.app.route(glob.config["git"]["webhook_endpoint"], methods = ["POST"])
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import os
|
||||
import json
|
||||
import shutil
|
||||
import mysql.connector
|
||||
import bcrypt
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Global variables that is None by default and gets overwritten in other modules
|
||||
|
||||
app = None # main.py -> Flask App
|
||||
sql_conn = None
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Global variables that initializes on first load of module
|
||||
|
@ -16,3 +19,18 @@ if not os.path.isfile("config.json"):
|
|||
|
||||
with open("config.json", "r") as f:
|
||||
config = json.load(f)
|
||||
|
||||
def make_sql_connection():
|
||||
return mysql.connector.connect(**config["mysql"])
|
||||
|
||||
def get_sql_connection():
|
||||
global sql_conn
|
||||
if sql_conn is None or not sql_conn.is_connected():
|
||||
sql_conn = make_sql_connection()
|
||||
return sql_conn
|
||||
|
||||
def hash_password(password):
|
||||
return bcrypt.hashpw(password.encode(), bcrypt.gensalt(10, prefix=b"2a")).decode()
|
||||
|
||||
def check_password(p1, p2):
|
||||
return bcrypt.checkpw(p1.encode(), p2.encode())
|
||||
|
|
|
@ -1,2 +1,6 @@
|
|||
Flask>=1.0.2
|
||||
Flask-WTF>=0.14.2
|
||||
flask_login>=0.4.1
|
||||
Flask-Babel>=0.12.2
|
||||
mysql-connector
|
||||
bcrypt
|
174
routes.py
174
routes.py
|
@ -1,16 +1,180 @@
|
|||
from flask import render_template, url_for, request
|
||||
from flask import render_template, url_for, request, redirect, flash, abort
|
||||
from wtforms import Form, BooleanField, StringField, PasswordField, validators
|
||||
import flask_login
|
||||
|
||||
from forms.login import LoginForm, RegisterForm, BillForm, WarrantyForm, ServiceForm, User, register_account
|
||||
|
||||
from objects import glob # Global sharing of python objects in a manageable way
|
||||
|
||||
from flask_babel import gettext
|
||||
|
||||
login_manager = flask_login.LoginManager()
|
||||
login_manager.init_app(glob.app)
|
||||
login_manager.login_view = "login"
|
||||
|
||||
logged_in_users = []
|
||||
|
||||
@glob.app.route("/")
|
||||
@glob.app.route("/home")
|
||||
@glob.app.route("/dashboard")
|
||||
def home():
|
||||
@flask_login.login_required
|
||||
def dashboard():
|
||||
return render_template("pages/dashboard.html")
|
||||
|
||||
@glob.app.route("/bills", methods = ["GET", "POST"])
|
||||
@flask_login.login_required
|
||||
def bills():
|
||||
form = BillForm(request.form)
|
||||
|
||||
conn = glob.make_sql_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
if request.method == "POST" and form.validate():
|
||||
cur.execute("""
|
||||
INSERT
|
||||
INTO Regninger
|
||||
VALUES (NULL, %s, %s, %s, %s, %s, 0, 1, %s)
|
||||
""", (form.payment_to.data, form.description.data, form.kid.data, form.sum.data, form.date_due.data, flask_login.current_user.id))
|
||||
|
||||
conn.commit()
|
||||
|
||||
return redirect(url_for("bills"))
|
||||
|
||||
cur.execute("""
|
||||
SELECT Betaletil, Regningfor, Regningsum, KID, Betalingsfrist, Betalt
|
||||
FROM Regninger
|
||||
WHERE BrukerID = %s
|
||||
""", (flask_login.current_user.id,))
|
||||
|
||||
data = []
|
||||
for row in cur:
|
||||
data.append(row)
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
return render_template("pages/bills.html", data=data, form=form)
|
||||
|
||||
@glob.app.route("/warranties", methods = ["GET", "POST"])
|
||||
@flask_login.login_required
|
||||
def warranties():
|
||||
form = WarrantyForm(request.form)
|
||||
|
||||
conn = glob.make_sql_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
if request.method == "POST" and form.validate():
|
||||
cur.execute("""
|
||||
INSERT
|
||||
INTO Garanti
|
||||
VALUES (NULL, %s, %s, %s, %s, 1, %s)
|
||||
""", (form.item.data, form.date_from.data, form.date_to.data, form.sum.data, flask_login.current_user.id))
|
||||
|
||||
conn.commit()
|
||||
|
||||
return redirect(url_for("warranties"))
|
||||
|
||||
cur.execute("""
|
||||
SELECT Vare, Kjøpsdato, Garantitil, Pris
|
||||
FROM Garanti
|
||||
WHERE BrukerID = %s
|
||||
""", (flask_login.current_user.id,))
|
||||
|
||||
data = []
|
||||
for row in cur:
|
||||
data.append(row)
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
return render_template("pages/warranties.html", data=data, form=form)
|
||||
|
||||
@glob.app.route("/receipts", methods = ["GET", "POST"])
|
||||
@flask_login.login_required
|
||||
def receipts():
|
||||
return render_template("pages/receipts.html")
|
||||
|
||||
@glob.app.route("/services", methods = ["GET", "POST"])
|
||||
@flask_login.login_required
|
||||
def services():
|
||||
form = ServiceForm(request.form)
|
||||
|
||||
conn = glob.make_sql_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
if request.method == "POST" and form.validate():
|
||||
cur.execute("""
|
||||
INSERT
|
||||
INTO Services
|
||||
VALUES (NULL, %s, %s, %s, %s, 1, %s, %s)
|
||||
""", (form.name.data, form.type.data, form.contact.data, form.phone.data, flask_login.current_user.id, form.website.data))
|
||||
|
||||
conn.commit()
|
||||
|
||||
return redirect(url_for("services"))
|
||||
|
||||
cur.execute("""
|
||||
SELECT ServiceName, ServiceType, Kontaktperson, Telefonnummer, Hjemmeside
|
||||
FROM Services
|
||||
WHERE BrukerID = %s
|
||||
""", (flask_login.current_user.id,))
|
||||
|
||||
data = []
|
||||
for row in cur:
|
||||
data.append(row)
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
return render_template("pages/services.html", data=data, form=form)
|
||||
|
||||
@glob.app.route("/login", methods = ["GET", "POST"])
|
||||
def serve_login():
|
||||
def login():
|
||||
if flask_login.current_user.is_authenticated:
|
||||
flash(gettext("Already logged in"), "info")
|
||||
return redirect(url_for("dashboard"))
|
||||
|
||||
form_login = LoginForm(request.form)
|
||||
form_register = RegisterForm(request.form)
|
||||
|
||||
if request.method == "POST":
|
||||
return "TODO: Login handle", 501
|
||||
return render_template("login.html")
|
||||
if form_register.validate():
|
||||
try:
|
||||
register_account(form_register.email.data, form_register.password.data, form_register.firstname.data, form_register.surname.data)
|
||||
flash(gettext("User registered"), "success")
|
||||
except Exception as e:
|
||||
flash(gettext(str(e)), "danger")
|
||||
return redirect(url_for("login"))
|
||||
elif form_login.validate():
|
||||
try:
|
||||
user = User((form_login.email.data, form_login.password.data))
|
||||
flask_login.login_user(user)
|
||||
logged_in_users.append(user)
|
||||
flash(gettext("Logged in"), "success")
|
||||
except Exception as e:
|
||||
flash(gettext(str(e)), "danger")
|
||||
return redirect(url_for("login"))
|
||||
return redirect(url_for("dashboard")) # Valid login > Redirect to dashboard as user is logged in
|
||||
return render_template("login.html", form = {
|
||||
"login": form_login,
|
||||
"register": form_register
|
||||
})
|
||||
|
||||
@glob.app.route("/logout")
|
||||
@flask_login.login_required
|
||||
def logout():
|
||||
flask_login.logout_user()
|
||||
flash(gettext("Logged out"), "success")
|
||||
return redirect(url_for("login"))
|
||||
|
||||
@glob.app.errorhandler(401)
|
||||
def unauthorized_handler_err():
|
||||
flash(gettext("Login is required"), "danger")
|
||||
unauthorized_handler()
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(uuid):
|
||||
uuid = int(uuid)
|
||||
lst = [x for x in logged_in_users if x.id == uuid]
|
||||
return lst[0] if len(lst) > 0 else None
|
||||
|
||||
@login_manager.unauthorized_handler
|
||||
def unauthorized_handler():
|
||||
return redirect(url_for("login"))
|
||||
|
|
|
@ -120,7 +120,7 @@ html[xmlns] .clearfix {
|
|||
.calendar .clndr .clndr-table .header-days {
|
||||
height: 30px;
|
||||
font-size: 10px;
|
||||
background: #0D70A6;
|
||||
background: #506EE4;
|
||||
}
|
||||
.calendar .clndr .clndr-table .header-days .header-day {
|
||||
vertical-align: middle;
|
||||
|
@ -139,8 +139,7 @@ html[xmlns] .clearfix {
|
|||
vertical-align: top;
|
||||
}
|
||||
.calendar .clndr .clndr-table tr .day {
|
||||
border-left: 1px solid #000000;
|
||||
border-top: 1px solid #000000;
|
||||
border: 1px solid #000000;
|
||||
width: 100%;
|
||||
height: inherit;
|
||||
}
|
||||
|
@ -149,23 +148,15 @@ html[xmlns] .clearfix {
|
|||
}
|
||||
.calendar .clndr .clndr-table tr .day.today,
|
||||
.calendar .clndr .clndr-table tr .day.my-today {
|
||||
background: #9AD6E3;
|
||||
}
|
||||
.calendar .clndr .clndr-table tr .day.today:hover,
|
||||
.calendar .clndr .clndr-table tr .day.my-today:hover {
|
||||
background: #72c6d8;
|
||||
}
|
||||
.calendar .clndr .clndr-table tr .day.today.event,
|
||||
.calendar .clndr .clndr-table tr .day.my-today.event {
|
||||
background: #a7dbc1;
|
||||
box-shadow: inset 0 0 0 2px black;
|
||||
}
|
||||
.calendar .clndr .clndr-table tr .day.event,
|
||||
.calendar .clndr .clndr-table tr .day.my-event {
|
||||
background: #B4E09F;
|
||||
background: #a0b3ff;
|
||||
}
|
||||
.calendar .clndr .clndr-table tr .day.event:hover,
|
||||
.calendar .clndr .clndr-table tr .day.my-event:hover {
|
||||
background: #96d478;
|
||||
background: #91a3eb;
|
||||
}
|
||||
.calendar .clndr .clndr-table tr .day.inactive,
|
||||
.calendar .clndr .clndr-table tr .day.my-inactive {
|
||||
|
@ -228,3 +219,28 @@ html[xmlns] .clearfix {
|
|||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
.calendar .clndr .clndr-table tr .day.selected {
|
||||
outline: 2px dotted black;
|
||||
outline-offset: -4px;
|
||||
}
|
||||
*/
|
||||
|
||||
.calendar {
|
||||
height: 578px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.clndr > .clndr-main > .events {
|
||||
display: none;
|
||||
|
||||
position: relative;
|
||||
top: -541px;
|
||||
height: 541px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.clndr > .clndr-main.show-event-menu > .events {
|
||||
display: block;
|
||||
}
|
|
@ -5,3 +5,19 @@ html, body {
|
|||
background-color: #eee;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
.container.module {
|
||||
background-color: white;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 25%);
|
||||
grid-template-rows: repeat(4, 25%);
|
||||
grid-column-gap: 8px;
|
||||
grid-row-gap: 8px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
|
|
BIN
static/img/login-bg.jpg
Normal file
BIN
static/img/login-bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 276 KiB |
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
{% include 'layout/includes/boot-head.html' %}
|
||||
{% if title %}
|
||||
<title>Husstanden - {{ title }}</title>
|
||||
<title>Husstanden - {{ _(title) }}</title>
|
||||
{% else %}
|
||||
<title>Husstanden</title>
|
||||
{% endif %}
|
||||
|
|
|
@ -4,20 +4,42 @@
|
|||
{% include 'layout/includes/boot-head.html' %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/custom.css') }}">
|
||||
{% if title %}
|
||||
<title>Husstanden - {{ title }}</title>
|
||||
<title>Husstanden - {{ _(title) | title }}</title>
|
||||
{% else %}
|
||||
<title>Husstanden</title>
|
||||
{% endif %}
|
||||
<style>
|
||||
#sidenav-container {
|
||||
flex: 0 0 320px;
|
||||
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px){
|
||||
#sidenav-container {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1072px){
|
||||
#sidenav-container {
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="height:100%">
|
||||
<div class="p-2" style="height:100%">
|
||||
<div class="d-flex" style="height:100%">
|
||||
<div class="col p-2" style="flex:0 0 320px;">
|
||||
<div class="col p-2" id="sidenav-container">
|
||||
{% include 'layout/includes/side_nav.html' %}
|
||||
</div>
|
||||
<div class="col p-2">
|
||||
{% include 'layout/includes/top_nav.html' %}
|
||||
<div class="content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
}
|
||||
|
||||
#sidenav {
|
||||
background-color: #B507DB;
|
||||
background-color: #506EE4;
|
||||
border-radius: 4px;
|
||||
|
||||
z-index: 100;
|
||||
|
@ -30,7 +30,7 @@
|
|||
}
|
||||
|
||||
#sidenav a {
|
||||
color: #e9d9fc;
|
||||
color: #C5CBE2;
|
||||
line-height: 1;
|
||||
}
|
||||
#sidenav a:hover {
|
||||
|
@ -104,55 +104,41 @@
|
|||
<div class="p-2"> <!-- Padding -->
|
||||
<div class="flex-column">
|
||||
<div> <!-- Collection -->
|
||||
<a class="item" onclick="toggleCategory(this)">
|
||||
<i class="fa fa-chart-bar"></i><span>Dashboard</span>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
<a class="item" href="{{ url_for('dashboard') }}">
|
||||
<i class="far fa-calendar-alt"></i><span>{{ _("Dashboard") }}</span>
|
||||
</a>
|
||||
<div class="downtab hidden">
|
||||
<div class="flex-column">
|
||||
<div class="page">
|
||||
<a href="#dash-test">Test</a>
|
||||
</div>
|
||||
<div class="page">
|
||||
<a href="#dash-demo">Demo</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Title card</h4>
|
||||
<h4>{{ _("Economical") }}</h4>
|
||||
</div>
|
||||
<div>
|
||||
<a class="item" onclick="toggleCategory(this)">
|
||||
<i class="fa fa-chart-bar"></i><span>Bills</span>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
<a class="item" href="{{ url_for('bills') }}">
|
||||
<i class="fas fa-money-check-alt"></i><span>{{ _("Bills") }}</span>
|
||||
</a>
|
||||
<div class="downtab hidden">
|
||||
<div class="flex-column">
|
||||
<div class="page">
|
||||
<a href="#bills-bills">Bills</a>
|
||||
</div>
|
||||
<div class="page">
|
||||
<a href="#bills-log">Log</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a class="item" onclick="toggleCategory(this)">
|
||||
<i class="fa fa-chart-bar"></i><span>Recieps</span>
|
||||
<i class="far fa-list-alt"></i><span>{{ _("Receipts") }}</span>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
<div class="downtab hidden">
|
||||
<div class="flex-column">
|
||||
<div class="page">
|
||||
<a href="#recieps-recieps">Recieps</a>
|
||||
<a href="{{ url_for('receipts') }}">{{ _("Receipts") }}</a>
|
||||
</div>
|
||||
<div class="page">
|
||||
<a href="#recieps-log">Log</a>
|
||||
<a href="{{ url_for('warranties') }}">{{ _("Warranties") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4>{{ _("Contacts") }}</h4>
|
||||
</div>
|
||||
<div>
|
||||
<a class="item" href="{{ url_for('services') }}">
|
||||
<i class="fas fa-tools"></i><span>{{ _("Services") }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<style>
|
||||
.rndblock {
|
||||
border-radius: 100%;
|
||||
background: #B507DB;
|
||||
background: #506EE4;
|
||||
background-position: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
@ -11,13 +11,28 @@
|
|||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
display: table-cell;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.languk {
|
||||
.lang-icon {
|
||||
background-position: left;
|
||||
background-size: contain;
|
||||
width: 32px;
|
||||
height: 24px;
|
||||
|
||||
color: white;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
.lang-en {
|
||||
background-image: url("{{ url_for('static', filename='const/img/flags/gb.svg') }}");
|
||||
}
|
||||
|
||||
.langno {
|
||||
.lang-no {
|
||||
background-image: url("{{ url_for('static', filename='const/img/flags/no.svg') }}");
|
||||
}
|
||||
|
||||
|
@ -32,10 +47,42 @@
|
|||
</style>
|
||||
|
||||
<nav class="navbar topnav" style="margin-bottom: 10px;">
|
||||
<h3>{{ title }}</h3>
|
||||
<h3>{{ _(title) | title }}</h3>
|
||||
<div>
|
||||
{% with messages = get_flashed_messages(with_categories = true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} fade in show" role="alert" style="margin-bottom:0px;padding:6px;">
|
||||
<a href="#" class="close" data-dismiss="alert" aria-label="close" style="margin-left:10px;">×</a>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="my-2 my-lg-0 d-flex icon-buttons">
|
||||
<div class="col">
|
||||
<div class="rndblock languk"></div>
|
||||
<div class="rndblock lang-{{ session.lang }}" data-toggle="dropdown" aria-expanded="false"></div>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<div class="row">
|
||||
<a class="col d-flex m-1" href="?lang=en">
|
||||
<div class="lang-icon lang-en"></div>
|
||||
<div class="flex-grow-1"></div>
|
||||
<span>{{ _("english") | title }}</span>
|
||||
<div class="flex-grow-1"></div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a class="col d-flex m-1" href="?lang=no">
|
||||
<div class="lang-icon lang-no"></div>
|
||||
<div class="flex-grow-1"></div>
|
||||
<span>{{ _("norwegian") | title }}</span>
|
||||
<div class="flex-grow-1"></div>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="rndblock">
|
||||
|
@ -43,9 +90,18 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="rndblock"> <!-- OR INCLUDE BACKGROUND TEXTURE -->
|
||||
T <!-- FIRST CHAR OF USER -->
|
||||
<div class="rndblock" data-toggle="dropdown" aria-expanded="false"> <!-- OR INCLUDE BACKGROUND TEXTURE -->
|
||||
{{ current_user.firstname[0]|upper }} <!-- FIRST CHAR OF USER -->
|
||||
</div>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="{{ url_for('logout') }}">{{ _("Sign out") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
|
@ -1,40 +1,254 @@
|
|||
{% set title = "Login" %}
|
||||
{% set title = _("Login") %}
|
||||
|
||||
{% extends "layout/bootstrap.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container pt-3">
|
||||
<div class="row alert-section">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card rounded-1">
|
||||
<div class="card-header">
|
||||
<h3 class="mb-0">Login Methods</h3>
|
||||
</div>
|
||||
<div class="card-body row">
|
||||
<div class="col-md-6">
|
||||
<h5>Electronic ID</h5>
|
||||
<p class="lead">
|
||||
Secure login using Electronic ID allows Husstanden to show you banking details, bills and receipts.<br>
|
||||
<a target="_blank" href="https://eid.difi.no/en/id-porten/how-obtain-electronic-id">How to obtain an Electronic ID</a>
|
||||
</p>
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.main-head{
|
||||
height: 150px;
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
.sidenav {
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
overflow-x: hidden;
|
||||
padding-top: 20px;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: 25% 50% 25%;
|
||||
height: 100%;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidenav > div:nth-child(1n) {
|
||||
grid-row-start: 1;
|
||||
}
|
||||
|
||||
.sidenav > div:nth-child(2n) {
|
||||
grid-row-start: 2;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.sidenav > div:nth-child(3n) {
|
||||
grid-row-start: 3;
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
#main {
|
||||
padding: 0px 10px;
|
||||
display: grid;
|
||||
grid-template-rows: 25% auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#main > .col:nth-child(1n) {
|
||||
grid-row-start: 1;
|
||||
}
|
||||
|
||||
#main > .col:nth-child(2n) {
|
||||
grid-row-start: 2;
|
||||
}
|
||||
|
||||
#focus-login {
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media screen and (max-height: 450px) {
|
||||
.sidenav {padding-top: 15px;}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 450px) {
|
||||
.login-form{
|
||||
margin-top: 10%;
|
||||
}
|
||||
|
||||
.register-form{
|
||||
margin-top: 10%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px){
|
||||
#main{
|
||||
margin-left: 40%;
|
||||
}
|
||||
|
||||
.sidenav {
|
||||
width: 40%;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#focus-login {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.login-main-text{
|
||||
padding: 60px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.login-main-text h2{
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.btn-black{
|
||||
background-color: #000 !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hidable[aria-expanded="true"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggle-form > form:nth-child(2n) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggle-form.toggled > form:nth-child(1n) {
|
||||
display: none;
|
||||
}
|
||||
.toggle-form.toggled > form:nth-child(2n) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lang-icon {
|
||||
background-position: left;
|
||||
background-size: contain;
|
||||
width: 32px;
|
||||
height: 24px;
|
||||
|
||||
color: white;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
.lang-en {
|
||||
background-image: url("{{ url_for('static', filename='const/img/flags/gb.svg') }}");
|
||||
}
|
||||
|
||||
.lang-no {
|
||||
background-image: url("{{ url_for('static', filename='const/img/flags/no.svg') }}");
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
|
||||
.dropdown-menu a {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function toggleform(caller) {
|
||||
let selectedDOM = caller;
|
||||
do {
|
||||
if (selectedDOM.classList.contains("toggle-form")){
|
||||
let classes = selectedDOM.classList;
|
||||
classes[classes.contains("toggled") ? "remove" : "add"]("toggled");
|
||||
return;
|
||||
}
|
||||
selectedDOM = selectedDOM.parentElement;
|
||||
} while (selectedDOM != null);
|
||||
throw Error("Missing toggle-form class for self/parent(s)");
|
||||
}
|
||||
</script>
|
||||
<div class="sidenav">
|
||||
<div class="login-main-text">
|
||||
<h2>Husstanden<br>{{ _("Login Page") }}</h2>
|
||||
<p>{{ _("Login is required to use this service.") }}</p>
|
||||
</div>
|
||||
<div class="text-center mx-auto">
|
||||
<a href="#main" class="display-1" id="focus-login">Login</a>
|
||||
</div>
|
||||
<div class="btn-group dropup m-2">
|
||||
<div>
|
||||
<div class="btn btn-secondary col d-flex m-1" data-toggle="dropdown" aria-expanded="false">
|
||||
<div class="lang-icon lang-{{ session.lang }}"></div>
|
||||
<div class="flex-grow-1"></div>
|
||||
<span>{{ _("Language") }}</span>
|
||||
<div class="flex-grow-1"></div>
|
||||
</div>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<div class="row">
|
||||
<a class="col d-flex m-1" href="?lang=en">
|
||||
<div class="lang-icon lang-en"></div>
|
||||
<div class="flex-grow-1"></div>
|
||||
<span>{{ _("english") | title }}</span>
|
||||
<div class="flex-grow-1"></div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h5>House Account</h5>
|
||||
<p class="lead">
|
||||
Accounts with less privileges and fast login to view non-sensitive information.<br>
|
||||
Multiple house accounts can be created by logging in with house holder's verified <i>Electronic ID</i>.
|
||||
</p>
|
||||
<div class="row">
|
||||
<a class="col d-flex m-1" href="?lang=no">
|
||||
<div class="lang-icon lang-no"></div>
|
||||
<div class="flex-grow-1"></div>
|
||||
<span>{{ _("norwegian") | title }}</span>
|
||||
<div class="flex-grow-1"></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="card-body row pt-2">
|
||||
<div class="col-md-6 pb-2">
|
||||
<a class="btn btn-primary btn-lg btn-block" href="#TODO_LOGIN_EID" onclick="alertAbove(this, 'warning', 'This should redirect the user to Electronic ID page (but due to phishing, we can not demo this)')">Electronic ID</a>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<a class="btn btn-primary btn-lg btn-block" id="test" href="#TODO_LOGIN_HA" onclick="alertAbove(this, 'info', 'Unimplemented - Awaiting database')">House Account</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main">
|
||||
<div class="col pt-4">
|
||||
{% with messages = get_flashed_messages(with_categories = true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} fade in show" role="alert" style="margin-bottom:0px;padding:6px;">
|
||||
<a href="#" class="close" data-dismiss="alert" aria-label="close" style="margin-left:10px;">×</a>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="col col-md-6 col-sm-12">
|
||||
<div class="login-form">
|
||||
{% macro render_field(field) %}
|
||||
<div class="form-group">
|
||||
<label for="{{ field.label.field_id }}">{{ _(field.label.text) }}</label>
|
||||
{{ field(**kwargs)|safe }}
|
||||
{% if field.errors %}
|
||||
<ul class=errors>
|
||||
{% for error in field.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
<div class="toggle-form false">
|
||||
<form method="post">
|
||||
{{ render_field(form.login.email) }}
|
||||
{{ render_field(form.login.password) }}
|
||||
<button class="btn btn-black">{{ _("Login") }}</button>
|
||||
<span class="btn btn-secondary" onclick="toggleform(this)">{{ _("Register") }}</span>
|
||||
</form>
|
||||
<form method="post">
|
||||
{{ render_field(form.register.email) }}
|
||||
{{ render_field(form.register.password) }}
|
||||
{{ render_field(form.register.confirm_password) }}
|
||||
{{ render_field(form.register.firstname) }}
|
||||
{{ render_field(form.register.surname) }}
|
||||
{{ render_field(form.register.accept_tos) }}
|
||||
<span class="btn btn-secondary" onclick="toggleform(this)">{{ _("Login") }}</span>
|
||||
<button class="btn btn-black">{{ _("Register") }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,21 +1,89 @@
|
|||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/calendar.css') }}">
|
||||
|
||||
<div class="calendar"></div>
|
||||
<div class="calendar">
|
||||
<script type="text/template" id="tmplt-clndr">
|
||||
<div class='clndr-controls'>
|
||||
<div class='clndr-control-button'>
|
||||
<span class='clndr-previous-button'>‹ previous</span>
|
||||
</div>
|
||||
<div class='month'><%= month %> <%= year %></div>
|
||||
<div class='clndr-control-button rightalign'>
|
||||
<span class='clndr-next-button'>next ›</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clndr-main">
|
||||
<table class='clndr-table' border='0' cellspacing='0' cellpadding='0'>
|
||||
<thead>
|
||||
<tr class='header-days'>
|
||||
<% for(var i = 0; i < daysOfTheWeek.length; i++) { %>
|
||||
<td class='header-day'><%= daysOfTheWeek[i] %></td>
|
||||
<% } %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% for(var i = 0; i < numberOfRows; i++){ %>
|
||||
<tr>
|
||||
<% for(var j = 0; j < 7; j++){ %>
|
||||
<% var d = j + i * 7; %>
|
||||
<td class='<%= days[d].classes %>'>
|
||||
<div class='day-contents'><%= days[d].day %></div>
|
||||
</td>
|
||||
<% } %>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/template" id="tmplt-clndr-events">
|
||||
<div class='clndr-controls'>
|
||||
<div class='clndr-control-button'>
|
||||
<span class='clndr-previous-button'>‹ previous</span>
|
||||
</div>
|
||||
<div class='month'><%= extras.selected.format("LL") %></div>
|
||||
<div class='clndr-control-button rightalign'>
|
||||
<span class='clndr-next-button'>next ›</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clndr-main">
|
||||
<table class='clndr-table' border='0' cellspacing='0' cellpadding='0'>
|
||||
<thead>
|
||||
<tr class='header-days'>
|
||||
<% for(var i = 0; i < daysOfTheWeek.length; i++) { %>
|
||||
<td class='header-day'><%= daysOfTheWeek[i] %></td>
|
||||
<% } %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% for(var i = 0; i < numberOfRows; i++){ %>
|
||||
<tr>
|
||||
<% for(var j = 0; j < 7; j++){ %>
|
||||
<% var d = j + i * 7; %>
|
||||
<td class='<%= days[d].classes %>'>
|
||||
<div class='day-contents'><%= days[d].day %></div>
|
||||
</td>
|
||||
<% } %>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/moment.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/underscore.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='modules/clndr.min.js') }}"></script>
|
||||
<script>
|
||||
var calendars = {};
|
||||
var tmplts = {};
|
||||
var a;
|
||||
|
||||
$(document).ready( function() {
|
||||
// Assuming you've got the appropriate language files,
|
||||
// clndr will respect whatever moment's language is set to.
|
||||
// moment.locale('ru');
|
||||
tmplts.clndr = $('#tmplt-clndr').html();
|
||||
tmplts.clndrEvents = $('#tmplt-clndr-events').html();
|
||||
|
||||
// Here's some magic to make sure the dates are happening this month.
|
||||
var thisMonth = moment().format('YYYY-MM');
|
||||
// Events to load into calendar
|
||||
var eventArray = [ // TODO: Get events from database
|
||||
{
|
||||
title: 'Multi-Day Event',
|
||||
|
@ -28,19 +96,28 @@ $(document).ready( function() {
|
|||
}, {
|
||||
date: thisMonth + '-27',
|
||||
title: 'Single Day Event'
|
||||
}
|
||||
}, {
|
||||
date: thisMonth + '-6',
|
||||
title: 'Test'
|
||||
}
|
||||
];
|
||||
|
||||
// The order of the click handlers is predictable. Direct click action
|
||||
// callbacks come first: click, nextMonth, previousMonth, nextYear,
|
||||
// previousYear, nextInterval, previousInterval, or today. Then
|
||||
// onMonthChange (if the month changed), inIntervalChange if the interval
|
||||
// has changed, and finally onYearChange (if the year changed).
|
||||
calendars.clndr = $('.calendar').clndr({
|
||||
events: eventArray,
|
||||
clickEvents: {
|
||||
click: function (target) {
|
||||
console.log('Cal-1 clicked: ', target);
|
||||
a = target;
|
||||
console.log(target.date.day());
|
||||
this.options.extras.selected = target.date;
|
||||
this.compiledClndrTemplate = _.template(tmplts.clndrEvents);
|
||||
this.render();
|
||||
/*
|
||||
var clndrContainer = $('.calendar').find('.clndr-main');
|
||||
clndrContainer.toggleClass('show-event-menu', true);
|
||||
$('.calendar').find('.x-button').click( function() {
|
||||
clndrContainer.toggleClass('show-event-menu', false);
|
||||
});
|
||||
*/
|
||||
},
|
||||
today: function () {
|
||||
console.log('Cal-1 today');
|
||||
|
@ -53,24 +130,6 @@ $(document).ready( function() {
|
|||
},
|
||||
onMonthChange: function () {
|
||||
console.log('Cal-1 month changed');
|
||||
},
|
||||
nextYear: function () {
|
||||
console.log('Cal-1 next year');
|
||||
},
|
||||
previousYear: function () {
|
||||
console.log('Cal-1 previous year');
|
||||
},
|
||||
onYearChange: function () {
|
||||
console.log('Cal-1 year changed');
|
||||
},
|
||||
nextInterval: function () {
|
||||
console.log('Cal-1 next interval');
|
||||
},
|
||||
previousInterval: function () {
|
||||
console.log('Cal-1 previous interval');
|
||||
},
|
||||
onIntervalChange: function () {
|
||||
console.log('Cal-1 interval changed');
|
||||
}
|
||||
},
|
||||
multiDayEvents: {
|
||||
|
@ -79,17 +138,19 @@ $(document).ready( function() {
|
|||
startDate: 'startDate'
|
||||
},
|
||||
showAdjacentMonths: true,
|
||||
adjacentDaysChangeMonth: false
|
||||
adjacentDaysChangeMonth: false,
|
||||
weekOffset: 1,
|
||||
forceSixRows: true,
|
||||
template: tmplts.clndr,
|
||||
extras: {
|
||||
selected: null
|
||||
}
|
||||
});
|
||||
|
||||
// Bind all clndrs to the left and right arrow keys
|
||||
$(document).keydown( function(e) {
|
||||
// Left arrow
|
||||
if (e.keyCode == 37) {
|
||||
calendars.clndr.back();
|
||||
}
|
||||
|
||||
// Right arrow
|
||||
if (e.keyCode == 39) {
|
||||
calendars.clndr.forward();
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
{% set title = "bill:)" %}
|
||||
|
||||
{% extends "layout/bootstrap.html" %}
|
||||
|
||||
{% block content %}
|
||||
<p>lotsa bills</p>
|
||||
<p>many bills</p>
|
||||
<i class="fas fa-user"></i> <!-- uses solid style -->
|
||||
<i class="far fa-user"></i> <!-- uses regular style -->
|
||||
<i class="fab fa-github-square"></i> <!-- uses brands style -->
|
||||
{% endblock %}
|
84
templates/pages/bills.html
Normal file
84
templates/pages/bills.html
Normal file
|
@ -0,0 +1,84 @@
|
|||
{% set title = _("Bills") %}
|
||||
|
||||
{% extends "layout/dash.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<style id="grid">
|
||||
.container.module:nth-child(1n) {
|
||||
grid-area: 1 / 1 / 5 / 5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container module">
|
||||
|
||||
<button type="button" class="btn btn-primary" style="margin:10px;" data-toggle="modal" data-target="#myModal">{{ _("Add") }}</button>
|
||||
<div class="modal fade" id="myModal" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<!-- Modal content-->
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ _("Add bill") }}</h4>
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% macro render_field(field) %}
|
||||
<div class="form-group">
|
||||
<label for="{{ field.label.field_id }}">{{ _(field.label.text) }}</label>
|
||||
{{ field(**kwargs)|safe }}
|
||||
{% if field.errors %}
|
||||
<ul class=errors>
|
||||
{% for error in field.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
<form method=post>
|
||||
<dl>
|
||||
{{ render_field(form.payment_to) }}
|
||||
{{ render_field(form.description) }}
|
||||
{{ render_field(form.sum) }}
|
||||
{{ render_field(form.kid) }}
|
||||
{{ render_field(form.date_due) }}
|
||||
</dl>
|
||||
<input type=submit value="{{ _('Add') }}">
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ _("Close") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th scope="col">{{ _("Payment to") }}</th>
|
||||
<th scope="col">{{ _("Description") }}</th>
|
||||
<th scope="col" style="width: 120px">{{ _("Sum") }}</th>
|
||||
<th scope="col" style="width: 220px">{{ _("KID") }}</th>
|
||||
<th scope="col" style="width: 120px">{{ _("Date due") }}</th>
|
||||
<th scope="col" style="width: 150px">{{ _("Payment status") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in data %}
|
||||
<!--<tr class="table-{{ 'success' if row[5] else 'danger' }}">-->
|
||||
<tr>
|
||||
<th>{{ row[0] }}</th>
|
||||
<td>{{ row[1] }}</td>
|
||||
<td>{{ row[2] }}</td>
|
||||
<td>{{ row[3] }}</td>
|
||||
<td>{{ row[4] }}</td>
|
||||
<td style="background-color:{{ '#4F4' if row[5] else '#F44' }};"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,9 +1,31 @@
|
|||
{% set title = "Dashboard" %}
|
||||
{% set title = _("Dashboard") %}
|
||||
|
||||
{% extends "layout/dash.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container module">
|
||||
<h4>Reminders</h4>
|
||||
</div>
|
||||
|
||||
<div class="container module">
|
||||
<h4>Calendar</h4>
|
||||
{% include "modules/calendar.html" %}
|
||||
</div>
|
||||
|
||||
<style id="grid">
|
||||
.container.module:nth-child(1n) {
|
||||
grid-area: 1 / 1 / 5 / 2;
|
||||
}
|
||||
.container.module:nth-child(2n) {
|
||||
grid-area: 1 / 2 / 5 / 5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function onDaySelect() {
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
|
@ -1,10 +0,0 @@
|
|||
{% set title = "Kvitteringer" %}
|
||||
|
||||
{% extends "layout/bootstrap.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<p>Tester siden!</p>
|
||||
<p>KVITTERINGER FOR FAEN!</p>
|
||||
|
||||
{% endblock %}
|
17
templates/pages/receipts.html
Normal file
17
templates/pages/receipts.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
{% set title = _("Receipts") %}
|
||||
|
||||
{% extends "layout/dash.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<style id="grid">
|
||||
.container.module:nth-child(1n) {
|
||||
grid-area: 1 / 1 / 5 / 5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container module">
|
||||
<a>Unfinished</a>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,8 +0,0 @@
|
|||
{% set title = "Regninger" %}
|
||||
|
||||
{% extends "layout/bootstrap.html" %}
|
||||
|
||||
{% block content %}
|
||||
<p>Regninger :D</p>
|
||||
<p>test</p>
|
||||
{% endblock %}
|
82
templates/pages/services.html
Normal file
82
templates/pages/services.html
Normal file
|
@ -0,0 +1,82 @@
|
|||
{% set title = _("Services") %}
|
||||
|
||||
{% extends "layout/dash.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<style id="grid">
|
||||
.container.module:nth-child(1n) {
|
||||
grid-area: 1 / 1 / 5 / 5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container module">
|
||||
|
||||
<button type="button" class="btn btn-primary" style="margin:10px;" data-toggle="modal" data-target="#myModal">{{ _("Add") }}</button>
|
||||
<div class="modal fade" id="myModal" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<!-- Modal content-->
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ _("Add service") }}</h4>
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% macro render_field(field) %}
|
||||
<div class="form-group">
|
||||
<label for="{{ field.label.field_id }}">{{ _(field.label.text) }}</label>
|
||||
{{ field(**kwargs)|safe }}
|
||||
{% if field.errors %}
|
||||
<ul class=errors>
|
||||
{% for error in field.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
<form method=post>
|
||||
<dl>
|
||||
{{ render_field(form.name) }}
|
||||
{{ render_field(form.type) }}
|
||||
{{ render_field(form.contact) }}
|
||||
{{ render_field(form.phone) }}
|
||||
{{ render_field(form.website) }}
|
||||
</dl>
|
||||
<input type=submit value="{{ _('Add') }}">
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ _("Close") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th scope="col">{{ _("Name") }}</th>
|
||||
<th scope="col">{{ _("Type") }}</th>
|
||||
<th scope="col">{{ _("Contact") }}</th>
|
||||
<th scope="col">{{ _("Phone") }}</th>
|
||||
<th scope="col">{{ _("Website") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in data %}
|
||||
<!--<tr class="table-{{ 'success' if row[5] else 'danger' }}">-->
|
||||
<tr>
|
||||
<th>{{ row[0] }}</th>
|
||||
<td>{{ row[1] }}</td>
|
||||
<td>{{ row[2] }}</td>
|
||||
<td>{{ row[3] }}</td>
|
||||
<td>{{ row[4] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,8 +0,0 @@
|
|||
{% set title = "testing" %}
|
||||
|
||||
{% extends "layout/bootstrap.html" %}
|
||||
|
||||
{% block content %}
|
||||
<p>testing</p>
|
||||
<p>tester dette</p>
|
||||
{% endblock %}
|
79
templates/pages/warranties.html
Normal file
79
templates/pages/warranties.html
Normal file
|
@ -0,0 +1,79 @@
|
|||
{% set title = _("Warranties") %}
|
||||
|
||||
{% extends "layout/dash.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<style id="grid">
|
||||
.container.module:nth-child(1n) {
|
||||
grid-area: 1 / 1 / 5 / 5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container module">
|
||||
|
||||
<button type="button" class="btn btn-primary" style="margin:10px;" data-toggle="modal" data-target="#myModal">{{ _("Add") }}</button>
|
||||
<div class="modal fade" id="myModal" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<!-- Modal content-->
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ _("Add warrenty") }}</h4>
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% macro render_field(field) %}
|
||||
<div class="form-group">
|
||||
<label for="{{ field.label.field_id }}">{{ _(field.label.text) }}</label>
|
||||
{{ field(**kwargs)|safe }}
|
||||
{% if field.errors %}
|
||||
<ul class=errors>
|
||||
{% for error in field.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
<form method=post>
|
||||
<dl>
|
||||
{{ render_field(form.item) }}
|
||||
{{ render_field(form.date_from) }}
|
||||
{{ render_field(form.date_to) }}
|
||||
{{ render_field(form.sum) }}
|
||||
</dl>
|
||||
<input type=submit value="{{ _('Add') }}">
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ _("Close") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th scope="col">{{ _("Item") }}</th>
|
||||
<th scope="col" style="width: 140px">{{ _("Date of purchase") }}</th>
|
||||
<th scope="col" style="width: 140px">{{ _("Warranty duration") }}</th>
|
||||
<th scope="col" style="width: 140px">{{ _("Sum") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in data %}
|
||||
<!--<tr class="table-{{ 'success' if row[5] else 'danger' }}">-->
|
||||
<tr>
|
||||
<th>{{ row[0] }}</th>
|
||||
<td>{{ row[1] }}</td>
|
||||
<td>{{ row[2] }}</td>
|
||||
<td>{{ row[3] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
55
templates/register.html
Normal file
55
templates/register.html
Normal file
|
@ -0,0 +1,55 @@
|
|||
{% set title = "Register" %}
|
||||
|
||||
{% extends "layout/bootstrap.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% macro render_field(field) %}
|
||||
<dt>{{ field.label }}
|
||||
<dd>{{ field(**kwargs)|safe }}
|
||||
{% if field.errors %}
|
||||
<ul class=errors>
|
||||
{% for error in field.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endmacro %}
|
||||
<script src="{{ url_for('static', filename='js/alerts.js') }}"></script>
|
||||
<div class="container pt-3">
|
||||
<div class="row alert-section">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card rounded-1">
|
||||
<div class="card-header">
|
||||
<h3 class="mb-0">Register</h3>
|
||||
</div>
|
||||
<div class="card-body row">
|
||||
<div class="col-md-6">
|
||||
<form method=post>
|
||||
<dl>
|
||||
{{ render_field(form.email) }}
|
||||
{{ render_field(form.password) }}
|
||||
{{ render_field(form.confirm_password) }}
|
||||
{{ render_field(form.firstname) }}
|
||||
{{ render_field(form.surname) }}
|
||||
{{ render_field(form.accept_tos) }}
|
||||
</dl>
|
||||
<input type=submit value="Register">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
let alertArea = document.getElementsByClassName("alert-section")[0];
|
||||
{% for message in messages %}
|
||||
alertAbove(alertArea, "info", "{{ message }}");
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</script>
|
||||
{% endblock %}
|
215
translations/nb/LC_MESSAGES/messages.po
Normal file
215
translations/nb/LC_MESSAGES/messages.po
Normal file
|
@ -0,0 +1,215 @@
|
|||
# Norwegian text and messages
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.4\n"
|
||||
"Report-Msgid-Bugs-To: noreply@osufx.com\n"
|
||||
"POT-Creation-Date: 2019-05-28 19:59+0200\n"
|
||||
"PO-Revision-Date: 2019-05-12 09:47+1000\n"
|
||||
"Last-Translator: Emily Steinsvik <emily@osufx.com>\n"
|
||||
"Language: no\n"
|
||||
"Language-Team: no <emily@osufx.com>\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.7.0\n"
|
||||
|
||||
#: routes.py:131
|
||||
msgid "Already logged in"
|
||||
msgstr "Allerede logget inn"
|
||||
|
||||
#: routes.py:141
|
||||
msgid "User registered"
|
||||
msgstr "Bruker registrert"
|
||||
|
||||
#: routes.py:150
|
||||
msgid "Logged in"
|
||||
msgstr "Logget inn"
|
||||
|
||||
#: routes.py:164
|
||||
msgid "Logged out"
|
||||
msgstr "Logget ut"
|
||||
|
||||
#: routes.py:169
|
||||
msgid "Login is required"
|
||||
msgstr "Innlogging kreves"
|
||||
|
||||
#: forms/login.py:14 templates/pages/bills.html:59
|
||||
msgid "Payment to"
|
||||
msgstr "Betaling til"
|
||||
|
||||
#: forms/login.py:15 templates/pages/bills.html:60
|
||||
msgid "Description"
|
||||
msgstr "Tekst"
|
||||
|
||||
#: forms/login.py:20 forms/login.py:28 templates/pages/bills.html:61
|
||||
#: templates/pages/warranties.html:61
|
||||
msgid "Sum"
|
||||
msgstr "Sum"
|
||||
|
||||
#: forms/login.py:21 templates/pages/bills.html:62
|
||||
msgid "KID"
|
||||
msgstr "KID"
|
||||
|
||||
#: forms/login.py:22 templates/pages/bills.html:63
|
||||
msgid "Date due"
|
||||
msgstr "Forfallsdato"
|
||||
|
||||
#: forms/login.py:25 templates/pages/warranties.html:58
|
||||
msgid "Item"
|
||||
msgstr "Vare"
|
||||
|
||||
#: forms/login.py:26 templates/pages/warranties.html:59
|
||||
msgid "Date of purchase"
|
||||
msgstr "Kjøpsdato"
|
||||
|
||||
#: forms/login.py:27 templates/pages/warranties.html:60
|
||||
msgid "Warranty duration"
|
||||
msgstr "Garanti til"
|
||||
|
||||
#: forms/login.py:31 templates/pages/services.html:59
|
||||
msgid "Name"
|
||||
msgstr "Navn"
|
||||
|
||||
#: forms/login.py:32 templates/pages/services.html:60
|
||||
msgid "Type"
|
||||
msgstr "Type"
|
||||
|
||||
#: forms/login.py:33 templates/pages/services.html:61
|
||||
msgid "Contact"
|
||||
msgstr "Kontaktperson"
|
||||
|
||||
#: forms/login.py:34 templates/pages/services.html:62
|
||||
msgid "Phone"
|
||||
msgstr "Telefon"
|
||||
|
||||
#: forms/login.py:35 templates/pages/services.html:63
|
||||
msgid "Website"
|
||||
msgstr "Nettside"
|
||||
|
||||
#: forms/login.py:38 forms/login.py:51
|
||||
msgid "Email"
|
||||
msgstr "Epost"
|
||||
|
||||
#: forms/login.py:44 forms/login.py:57
|
||||
msgid "Password"
|
||||
msgstr "Passord"
|
||||
|
||||
#: forms/login.py:60
|
||||
msgid "Passwords must match"
|
||||
msgstr "Passordene må være like"
|
||||
|
||||
#: forms/login.py:63
|
||||
msgid "Repeat Password"
|
||||
msgstr "Gjenta passord"
|
||||
|
||||
#: forms/login.py:65
|
||||
msgid "Firstname"
|
||||
msgstr "Fornavn"
|
||||
|
||||
#: forms/login.py:71
|
||||
msgid "Surname"
|
||||
msgstr "Etternavn"
|
||||
|
||||
#: forms/login.py:77
|
||||
msgid "I accept the TOS"
|
||||
msgstr "Jeg aksepterer TOS"
|
||||
|
||||
#: forms/login.py:104
|
||||
msgid "Invalid login"
|
||||
msgstr "Ugyldig innlogging"
|
||||
|
||||
#: forms/login.py:107
|
||||
msgid "Incorrect password"
|
||||
msgstr "Feil passord"
|
||||
|
||||
#: templates/login.html:1 templates/login.html:239 templates/login.html:249
|
||||
msgid "Login"
|
||||
msgstr "Login"
|
||||
|
||||
#: templates/login.html:170
|
||||
msgid "Login Page"
|
||||
msgstr "Innlogging"
|
||||
|
||||
#: templates/login.html:171
|
||||
msgid "Login is required to use this service."
|
||||
msgstr "Innlogging kreves for å bruke denne tjenesten."
|
||||
|
||||
#: templates/login.html:181
|
||||
msgid "Language"
|
||||
msgstr "Språk"
|
||||
|
||||
#: templates/layout/includes/top_nav.html:72 templates/login.html:190
|
||||
msgid "english"
|
||||
msgstr "engelsk"
|
||||
|
||||
#: templates/layout/includes/top_nav.html:80 templates/login.html:198
|
||||
msgid "norwegian"
|
||||
msgstr "norsk"
|
||||
|
||||
#: templates/login.html:240 templates/login.html:250
|
||||
msgid "Register"
|
||||
msgstr "Registrer"
|
||||
|
||||
#: templates/layout/includes/side_nav.html:108 templates/pages/dashboard.html:1
|
||||
msgid "Dashboard"
|
||||
msgstr "Dashbord"
|
||||
|
||||
#: templates/layout/includes/side_nav.html:112
|
||||
msgid "Economical"
|
||||
msgstr "Økonomisk"
|
||||
|
||||
#: templates/layout/includes/side_nav.html:116 templates/pages/bills.html:1
|
||||
msgid "Bills"
|
||||
msgstr "Regninger"
|
||||
|
||||
#: templates/layout/includes/side_nav.html:121
|
||||
#: templates/layout/includes/side_nav.html:127 templates/pages/receipts.html:1
|
||||
msgid "Receipts"
|
||||
msgstr "Kvitteringer"
|
||||
|
||||
#: templates/layout/includes/side_nav.html:130
|
||||
#: templates/pages/warranties.html:1
|
||||
msgid "Warranties"
|
||||
msgstr "Garantier"
|
||||
|
||||
#: templates/layout/includes/side_nav.html:136
|
||||
msgid "Contacts"
|
||||
msgstr "Kontakter"
|
||||
|
||||
#: templates/layout/includes/side_nav.html:140 templates/pages/services.html:1
|
||||
msgid "Services"
|
||||
msgstr "Service"
|
||||
|
||||
#: templates/layout/includes/top_nav.html:100
|
||||
msgid "Sign out"
|
||||
msgstr "Logg ut"
|
||||
|
||||
#: templates/pages/bills.html:15 templates/pages/bills.html:46
|
||||
#: templates/pages/services.html:15 templates/pages/services.html:46
|
||||
#: templates/pages/warranties.html:15 templates/pages/warranties.html:45
|
||||
msgid "Add"
|
||||
msgstr "Legg til"
|
||||
|
||||
#: templates/pages/bills.html:21
|
||||
msgid "Add bill"
|
||||
msgstr "Legg til regning"
|
||||
|
||||
#: templates/pages/bills.html:50 templates/pages/services.html:50
|
||||
#: templates/pages/warranties.html:49
|
||||
msgid "Close"
|
||||
msgstr "Lukk"
|
||||
|
||||
#: templates/pages/bills.html:64
|
||||
msgid "Payment status"
|
||||
msgstr "Betalingsstatus"
|
||||
|
||||
#: templates/pages/services.html:21
|
||||
msgid "Add service"
|
||||
msgstr "Legg til service"
|
||||
|
||||
#: templates/pages/warranties.html:21
|
||||
msgid "Add warrenty"
|
||||
msgstr "Legg til garanti"
|
||||
|
Loading…
Reference in New Issue
Block a user