Commit 3b4db2e4 by Damien Moulard

fix merge conflict

parents 7993a150 17a551fc
Pipeline #1927 passed with stage
in 1 minute 27 seconds
...@@ -385,14 +385,24 @@ def delete_shift_registration(request): ...@@ -385,14 +385,24 @@ def delete_shift_registration(request):
is_connected_user = CagetteUser.are_credentials_ok(request) is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True: if is_connected_user is True:
data = json.loads(request.body.decode()) data = json.loads(request.body.decode())
shift_registration_id = int(data["shift_registration_id"])
member_id = int(data["member_id"]) member_id = int(data["member_id"])
shift_registration_id = int(data["shift_registration_id"])
m = CagetteShift() shift_is_makeup = data["shift_is_makeup"]
res["res"] = m.cancel_shift([shift_registration_id])
# Note: 'upcoming_registration_count' in res.partner won't change because the _compute method # Note: 'upcoming_registration_count' in res.partner won't change because the _compute method
# in odoo counts canceled shift registrations. # in odoo counts canceled shift registrations.
m = CagetteShift()
res["cancel_shift"] = m.cancel_shift([shift_registration_id])
if shift_is_makeup is True:
fields = {
'name': "Admin BDM - Suppression d'un rattrapage",
'shift_id': False,
'type': data["member_shift_type"],
'partner_id': member_id,
'point_qty': 1
}
res["update_counter"] = m.update_counter_event(fields)
response = JsonResponse(res, safe=False) response = JsonResponse(res, safe=False)
else: else:
......
...@@ -13,6 +13,7 @@ import pytz ...@@ -13,6 +13,7 @@ import pytz
import locale import locale
import re import re
import dateutil.parser import dateutil.parser
from datetime import date
...@@ -351,6 +352,23 @@ class CagetteMember(models.Model): ...@@ -351,6 +352,23 @@ class CagetteMember(models.Model):
return answer return answer
@staticmethod @staticmethod
def is_associated(id_parent):
api = OdooAPI()
cond = [['parent_id', '=', int(id_parent)]]
fields = ['id','name','parent_id','birthdate']
res = api.search_read('res.partner', cond, fields, 10, 0, 'id DESC')
already_have_adult_associated = False
for partner in res:
birthdate = partner['birthdate']
if(birthdate):
today = date.today()
date1 = datetime.datetime.strptime(birthdate, "%Y-%m-%d")
age = today.year - date1.year - ((today.month, today.day) < (date1.month, date1.day))
if age > 17 :
already_have_adult_associated = True
return already_have_adult_associated
@staticmethod
def finalize_coop_creation(post_data): def finalize_coop_creation(post_data):
""" Update coop data. """ """ Update coop data. """
res = {} res = {}
...@@ -506,7 +524,30 @@ class CagetteMember(models.Model): ...@@ -506,7 +524,30 @@ class CagetteMember(models.Model):
m.create_capital_subscription_invoice(post_data['shares_euros'], today) m.create_capital_subscription_invoice(post_data['shares_euros'], today)
res['bc'] = m.generate_base_and_barcode(post_data) res['bc'] = m.generate_base_and_barcode(post_data)
# Create shift suscription # if the new member is associated with an already existing member
# then we put the state in "associated" and we create the "associated" member
if 'is_associated_people' in post_data and 'parent_id' in post_data :
fields = {}
fields['cooperative_state'] = 'associated'
api.update('res.partner', [partner_id], fields)
associated_member = {
'email': post_data['_id'],
'name': name,
'birthdate': birthdate,
'sex': sex,
'street': post_data['address'],
'zip': post_data['zip'],
'city': post_data['city'],
'phone': format_phone_number(post_data['mobile']), # Because list view default show Phone and people mainly gives mobile
'barcode_rule_id': settings.COOP_BARCODE_RULE_ID,
'parent_id' : post_data['parent_id'],
'is_associated_people': True
}
associated_member_id = api.create('res.partner', associated_member)
# If it's an new associated member with a new partner. Link will be made by the user in BDM/admin
# We add the associated member to the "associate" shift template so we can find them in Odoo
elif 'is_associated_people' not in post_data or 'is_associated_people' in post_data and 'parent_id' not in post_data:
# Create shift suscription if is not associated
shift_template = json.loads(post_data['shift_template']) shift_template = json.loads(post_data['shift_template'])
shift_t_id = shift_template['data']['id'] shift_t_id = shift_template['data']['id']
stype = shift_template['data']['type'] stype = shift_template['data']['type']
...@@ -735,9 +776,12 @@ class CagetteMember(models.Model): ...@@ -735,9 +776,12 @@ class CagetteMember(models.Model):
cond = [['name', 'ilike', str(key)]] cond = [['name', 'ilike', str(key)]]
cond.append('|') cond.append('|')
cond.append(['is_member', '=', True]) cond.append(['is_member', '=', True])
if search_type != 'members':
cond.append(['is_associated_people', '=', True]) cond.append(['is_associated_people', '=', True])
else:
cond.append(['is_associated_people', '=', False])
# cond.append(['cooperative_state', '!=', 'unsubscribed']) # cond.append(['cooperative_state', '!=', 'unsubscribed'])
if search_type == "full": if search_type == "full" or search_type == 'members':
fields = CagetteMember.m_default_fields fields = CagetteMember.m_default_fields
if not shift_id is None: if not shift_id is None:
CagetteMember.m_default_fields.append('tmpl_reg_line_ids') CagetteMember.m_default_fields.append('tmpl_reg_line_ids')
...@@ -865,6 +909,19 @@ class CagetteMember(models.Model): ...@@ -865,6 +909,19 @@ class CagetteMember(models.Model):
return res return res
def update_extra_shift_done(self, value):
api = OdooAPI()
res = {}
f = { 'extra_shift_done': value }
res_item = api.update('res.partner', [self.id], f)
res = {
'mid': self.id,
'update': res_item
}
return res
class CagetteMembers(models.Model): class CagetteMembers(models.Model):
"""Class to manage operations on all members or part of them.""" """Class to manage operations on all members or part of them."""
...@@ -1090,7 +1147,7 @@ class CagetteMembers(models.Model): ...@@ -1090,7 +1147,7 @@ class CagetteMembers(models.Model):
def get_makeups_members(): def get_makeups_members():
api = OdooAPI() api = OdooAPI()
cond = [['makeups_to_do','>', 0]] cond = [['makeups_to_do','>', 0]]
fields = ['id', 'name', 'makeups_to_do','shift_type'] fields = ['id', 'name', 'display_std_points', 'display_ftop_points', 'shift_type', 'makeups_to_do']
res = api.search_read('res.partner', cond, fields) res = api.search_read('res.partner', cond, fields)
return res return res
...@@ -1199,7 +1256,7 @@ class CagetteServices(models.Model): ...@@ -1199,7 +1256,7 @@ class CagetteServices(models.Model):
).total_seconds() / 60 > default_acceptable_minutes_after_shift_begins ).total_seconds() / 60 > default_acceptable_minutes_after_shift_begins
if with_members is True: if with_members is True:
cond = [['id', 'in', s['registration_ids']], ['state', 'not in', ['cancel', 'waiting', 'draft']]] cond = [['id', 'in', s['registration_ids']], ['state', 'not in', ['cancel', 'waiting', 'draft']]]
fields = ['partner_id', 'shift_type', 'state', 'is_late'] fields = ['partner_id', 'shift_type', 'state', 'is_late', 'associate_registered']
members = api.search_read('shift.registration', cond, fields) members = api.search_read('shift.registration', cond, fields)
s['members'] = sorted(members, key=lambda x: x['partner_id'][0]) s['members'] = sorted(members, key=lambda x: x['partner_id'][0])
if len(s['members']) > 0: if len(s['members']) > 0:
...@@ -1208,22 +1265,27 @@ class CagetteServices(models.Model): ...@@ -1208,22 +1265,27 @@ class CagetteServices(models.Model):
for m in s['members']: for m in s['members']:
mids.append(m['partner_id'][0]) mids.append(m['partner_id'][0])
cond = [['parent_id', 'in', mids]] cond = [['parent_id', 'in', mids]]
fields = ['parent_id', 'name'] fields = ['id', 'parent_id', 'name','barcode_base']
associated = api.search_read('res.partner', cond, fields) associated = api.search_read('res.partner', cond, fields)
if len(associated) > 0: if len(associated) > 0:
for m in s['members']: for m in s['members']:
for a in associated: for a in associated:
if int(a['parent_id'][0]) == int(m['partner_id'][0]): if int(a['parent_id'][0]) == int(m['partner_id'][0]):
m['partner_name'] = m['partner_id'][1]
m['partner_id'][1] += ' en binôme avec ' + a['name'] m['partner_id'][1] += ' en binôme avec ' + a['name']
m['associate_name'] = str(a['barcode_base']) + ' - ' + a['name']
return services return services
@staticmethod @staticmethod
def registration_done(registration_id, overrided_date=""): def registration_done(registration_id, overrided_date="", typeAction=""):
"""Equivalent to click present in presence form.""" """Equivalent to click present in presence form."""
api = OdooAPI() api = OdooAPI()
f = {'state': 'done'} f = {'state': 'done'}
if(typeAction != "normal" and typeAction != ""):
f['associate_registered'] = typeAction
late_mode = getattr(settings, 'ENTRANCE_WITH_LATE_MODE', False) late_mode = getattr(settings, 'ENTRANCE_WITH_LATE_MODE', False)
if late_mode is True: if late_mode is True:
# services = CagetteServices.get_services_at_time('14:28',0, with_members=False) # services = CagetteServices.get_services_at_time('14:28',0, with_members=False)
...@@ -1245,7 +1307,14 @@ class CagetteServices(models.Model): ...@@ -1245,7 +1307,14 @@ class CagetteServices(models.Model):
return api.update('shift.registration', [int(registration_id)], f) return api.update('shift.registration', [int(registration_id)], f)
@staticmethod @staticmethod
def record_rattrapage(mid, sid, stid): def reopen_registration(registration_id, overrided_date=""):
api = OdooAPI()
f = {'state': 'open'}
return api.update('shift.registration', [int(registration_id)], f)
@staticmethod
def record_rattrapage(mid, sid, stid, typeAction):
"""Add a shift registration for member mid. """Add a shift registration for member mid.
(shift sid, shift ticket stid) (shift sid, shift ticket stid)
...@@ -1261,6 +1330,8 @@ class CagetteServices(models.Model): ...@@ -1261,6 +1330,8 @@ class CagetteServices(models.Model):
"state": 'open'} "state": 'open'}
reg_id = api.create('shift.registration', fields) reg_id = api.create('shift.registration', fields)
f = {'state': 'done'} f = {'state': 'done'}
if(typeAction != "normal" and typeAction != ""):
f['associate_registered'] = typeAction
return api.update('shift.registration', [int(reg_id)], f) return api.update('shift.registration', [int(reg_id)], f)
@staticmethod @staticmethod
...@@ -1276,6 +1347,20 @@ class CagetteServices(models.Model): ...@@ -1276,6 +1347,20 @@ class CagetteServices(models.Model):
# let authorized people time to set presence for those who came in late # let authorized people time to set presence for those who came in late
end_date = now - datetime.timedelta(hours=2) end_date = now - datetime.timedelta(hours=2)
api = OdooAPI() api = OdooAPI()
# Let's start by adding an extra shift to associated member who came together
cond = [['date_begin', '>=', date_24h_before.isoformat()],
['date_begin', '<=', end_date.isoformat()],
['state', '=', 'done'], ['associate_registered', '=', 'both']]
fields = ['state', 'partner_id', 'date_begin']
res = api.search_read('shift.registration', cond, fields)
for r in res:
cond = [['id', '=', r['partner_id'][0]]]
fields = ['id','extra_shift_done']
res = api.search_read('res.partner', cond, fields)
f = {'extra_shift_done': res[0]['extra_shift_done'] + 1 }
api.update('res.partner', [r['partner_id'][0]], f)
absence_status = 'excused' absence_status = 'excused'
res_c = api.search_read('ir.config_parameter', res_c = api.search_read('ir.config_parameter',
[['key', '=', 'lacagette_membership.absence_status']], [['key', '=', 'lacagette_membership.absence_status']],
......
...@@ -38,6 +38,10 @@ ...@@ -38,6 +38,10 @@
padding-top: 0.755em; padding-top: 0.755em;
} }
.makeup_row {
background-color: #ffc854 !important;
}
/* Search membres area */ /* Search membres area */
#search_member_area { #search_member_area {
margin-top: 30px; margin-top: 30px;
......
...@@ -23,6 +23,57 @@ ...@@ -23,6 +23,57 @@
[data-week="4"] {border: 2px #eed000 solid;} [data-week="4"] {border: 2px #eed000 solid;}
#new_coop [name="email"] {width:25em;} #new_coop [name="email"] {width:25em;}
#new_coop{
max-width: 75%;
}
#mail_generation {position:absolute; bottom:30px;} #mail_generation {position:absolute; bottom:30px;}
#sex {padding: 0;} #sex {padding: 0;}
#existing_partner{
padding-right: 15px;
width: 45%;
}
#new_partner{
padding-left: 15px;
width: 45%;
}
#add_binome{
cursor: pointer;
text-decoration: underline;
font-weight: bold;
}
#existing_member_choice, #new_member_choice{
border: 1px solid black;
width: 40%;
cursor: pointer;
text-align: center;
max-width: 400px;
background-color: #e7e9ed;
color: black;
padding: 1rem 1.5rem;
}
#existing_member_choice:hover, #new_member_choice:hover{
background-color: #ccc;
}
.choice_active{
background-color: #0275d8 !important;
color: white !important;
}
#existing_member_choice{
margin-right: 15px;
}
.choice_button_area{
margin-bottom: 10px;
}
#associate_area{
margin-bottom: 15px;
}
\ No newline at end of file
...@@ -44,8 +44,9 @@ h1 .member_name {font-weight: bold;} ...@@ -44,8 +44,9 @@ h1 .member_name {font-weight: bold;}
.members_list {list-style: none;} .members_list {list-style: none;}
.members_list li {display:block;margin-bottom:5px;} .members_list li {display:block;margin-bottom:5px;}
.members_list li.btn--inverse {background: #449d44; cursor:not-allowed; color: #FFF; } .members_list li.btn--inverse {background: #449d44; color: #FFF; }
.members_list li.btn--inverse.late {background-color: #de9b00; color: white} .members_list li.btn--inverse.late {background-color: #de9b00; color: white}
.members_list li.btn--inverse.both {background-color: #0275d8 ; color: white}
#service_entry_success {font-size: x-large;} #service_entry_success {font-size: x-large;}
#service_entry_success .explanations {margin: 25px 0; font-size: 18px;} #service_entry_success .explanations {margin: 25px 0; font-size: 18px;}
...@@ -64,7 +65,7 @@ h1 .member_name {font-weight: bold;} ...@@ -64,7 +65,7 @@ h1 .member_name {font-weight: bold;}
#service_en_cours .info a {line-height: 24px; font-size:14px; margin-top:15px;} #service_en_cours .info a {line-height: 24px; font-size:14px; margin-top:15px;}
.outside_list a {margin-left:15px;} .outside_list a {margin-left:15px;}
#rattrapage_1 .advice {margin-top:15px;} #rattrapage_1 .advice {margin-top:15px;}
.btn.present {background:#50C878;} .btn.present {background:#50C878; color: white !important; font-weight: bold;}
.btn.return {background: #ff3333; color:#FFF !important; margin-top:25px;} .btn.return {background: #ff3333; color:#FFF !important; margin-top:25px;}
.msg-big {font-size: xx-large; background: #fff; padding:25px; text-align: center;} .msg-big {font-size: xx-large; background: #fff; padding:25px; text-align: center;}
......
...@@ -69,6 +69,19 @@ function display_makeups_members() { ...@@ -69,6 +69,19 @@ function display_makeups_members() {
title: "Nom" title: "Nom"
}, },
{ {
data: "shift_type",
title: "Nb de points",
className: "dt-body-center",
width: "10%",
render: function (data, type, row) {
if (data == 'ftop') {
return row.display_ftop_points
} else if (data == 'standard') {
return row.display_std_points
}
}
},
{
data: "makeups_to_do", data: "makeups_to_do",
title: "Nb rattrapages", title: "Nb rattrapages",
className: "dt-body-center", className: "dt-body-center",
......
...@@ -88,17 +88,29 @@ function display_member_shifts() { ...@@ -88,17 +88,29 @@ function display_member_shifts() {
"sLoadingRecords": "Chargement en cours...", "sLoadingRecords": "Chargement en cours...",
"sZeroRecords": "Aucun &eacute;l&eacute;ment &agrave; afficher", "sZeroRecords": "Aucun &eacute;l&eacute;ment &agrave; afficher",
"sEmptyTable": "Aucun futur service pour ce.tte membre" "sEmptyTable": "Aucun futur service pour ce.tte membre"
},
createdRow: function(row, rdata) {
if (rdata.is_makeup === true) {
$(row).addClass("makeup_row");
$(row).prop('title', 'Ce service est un rattrapage');
} }
},
}); });
$('#member_shifts_table').on('click', 'tbody td .delete_shift_registration', function () { $('#member_shifts_table').on('click', 'tbody td .delete_shift_registration', function () {
const row_data = member_shifts_table.row($(this).parents('tr')).data(); const row_data = member_shifts_table.row($(this).parents('tr')).data();
const shift_reg_id = row_data.id; const shift_reg_id = row_data.id;
const shift_is_makeup = row_data.is_makeup;
let msg = `<p>Enlever la présence de <b>${member.name}</b> au service du <b>${row_data.shift_id[1]}</b> ?</p>`;
if (shift_is_makeup === true) {
msg += `<p><i class="fas fa-exclamation-triangle"></i> Ce service est un rattrapage. Le supprimer ajoutera un point au compteur de ce.tte membre.</p>`;
}
openModal( openModal(
`Enlever la présence de ${member.name} au service du ${row_data.shift_id[1]} ?`, msg,
() => { () => {
delete_shift_registration(shift_reg_id); delete_shift_registration(shift_reg_id, shift_is_makeup);
}, },
"Confirmer", "Confirmer",
false false
...@@ -109,13 +121,16 @@ function display_member_shifts() { ...@@ -109,13 +121,16 @@ function display_member_shifts() {
/** /**
* Send request to delete shift registration * Send request to delete shift registration
* @param {Int} shift_reg_id Id of the shift_registration to delete * @param {Int} shift_reg_id Id of the shift_registration to delete
* @param {Boolean} shift_is_makeup Is the shift a makeup?
*/ */
function delete_shift_registration(shift_reg_id) { function delete_shift_registration(shift_reg_id, shift_is_makeup) {
openModal(); openModal();
data = { data = {
member_id: selected_member.id, member_id: selected_member.id,
shift_registration_id: shift_reg_id member_shift_type: selected_member.shift_type,
shift_registration_id: shift_reg_id,
shift_is_makeup: shift_is_makeup
}; };
$.ajax({ $.ajax({
......
...@@ -31,12 +31,14 @@ var search_field = $('input[name="search_string"]'); ...@@ -31,12 +31,14 @@ var search_field = $('input[name="search_string"]');
var shift_title = $('#current_shift_title'); var shift_title = $('#current_shift_title');
var shift_members = $('#current_shift_members'); var shift_members = $('#current_shift_members');
var service_validation = $('#service_validation'); var service_validation = $('#service_validation');
var associated_service_validation = $('#associated_service_validation');
var validation_last_call = 0; var validation_last_call = 0;
var rattrapage_wanted = $('[data-next="rattrapage_1"]'); var rattrapage_wanted = $('[data-next="rattrapage_1"]');
var webcam_is_attached = false; var webcam_is_attached = false;
var photo_advice = $('#photo_advice'); var photo_advice = $('#photo_advice');
var photo_studio = $('#photo_studio'); var photo_studio = $('#photo_studio');
var coop_info = $('.coop-info'); var coop_info = $('.coop-info');
var service_data = null;
const missed_begin_msg = $('#missed_begin_msg').html(); const missed_begin_msg = $('#missed_begin_msg').html();
...@@ -264,7 +266,7 @@ function get_simple_service_name(s) { ...@@ -264,7 +266,7 @@ function get_simple_service_name(s) {
} }
function move_service_validation_to(page) { function move_service_validation_to(page) {
service_validation.find('.btn').data('stid', '0'); service_data.stid=0;
page.find('.validation_wrapper') page.find('.validation_wrapper')
.append(service_validation.detach()); .append(service_validation.detach());
} }
...@@ -285,10 +287,14 @@ function fill_service_entry(s) { ...@@ -285,10 +287,14 @@ function fill_service_entry(s) {
var li_data = ""; var li_data = "";
if (e.state == "done") { if (e.state == "done") {
li_data = ' data-rid="'+e.id+'" data-mid="'+e.partner_id[0]+'"';
li_class += "--inverse"; li_class += "--inverse";
if (e.is_late == true) { if (e.is_late == true) {
li_class += " late"; li_class += " late";
} }
if (e.associate_registered=='both') {
li_class += " both";
}
} else { } else {
li_data = ' data-rid="'+e.id+'" data-mid="'+e.partner_id[0]+'"'; li_data = ' data-rid="'+e.id+'" data-mid="'+e.partner_id[0]+'"';
} }
...@@ -321,13 +327,29 @@ function clean_service_entry() { ...@@ -321,13 +327,29 @@ function clean_service_entry() {
function fill_service_validation(rid, coop_num_name, coop_id) { function fill_service_validation(rid, coop_num_name, coop_id) {
var coop_name_elts = coop_num_name.split(' - '); var coop_name_elts = coop_num_name.split(' - ');
for (member of loaded_services[0].members) {
if (member.id ==rid) {
if (member.associate_name) {
pages.service_entry_validation.find('#service_validation').hide();
pages.service_entry_validation.find('#associated_service_validation').show();
pages.service_entry_validation.find('#associated_btn').text(member.associate_name);
pages.service_entry_validation.find('#partner_btn').text(member.partner_name);
} else {
pages.service_entry_validation.find('#associated_service_validation').hide();
pages.service_entry_validation.find('#service_validation').show();
}
}
}
service_data={
rid: rid,
sid: selected_service.id,
mid: coop_id};
pages.service_entry_validation.find('span.member_name').text(coop_name_elts[1]); pages.service_entry_validation.find('span.member_name').text(coop_name_elts[1]);
move_service_validation_to(pages.service_entry_validation); move_service_validation_to(pages.service_entry_validation);
service_validation.find('.btn')
.data('rid', rid)
.data('sid', selected_service.id)
.data('mid', coop_id);
} }
function select_possible_service() { function select_possible_service() {
...@@ -372,7 +394,6 @@ function get_service_entry_data() { ...@@ -372,7 +394,6 @@ function get_service_entry_data() {
dataType : 'json' dataType : 'json'
}) })
.done(function(rData) { .done(function(rData) {
//console.log(rData);
info_place.text(''); info_place.text('');
var page_title = pages.service_entry.find('h1'); var page_title = pages.service_entry.find('h1');
...@@ -397,6 +418,7 @@ function get_service_entry_data() { ...@@ -397,6 +418,7 @@ function get_service_entry_data() {
page_title.text('Quel est ton service ?'); page_title.text('Quel est ton service ?');
} else { } else {
loaded_services = rData.res;
fill_service_entry(rData.res[0]); fill_service_entry(rData.res[0]);
} }
} }
...@@ -443,22 +465,21 @@ function fill_service_entry_sucess(member) { ...@@ -443,22 +465,21 @@ function fill_service_entry_sucess(member) {
} }
function record_service_presence() { function record_service_presence(e) {
var d = new Date(); var d = new Date();
var elapsed_since_last_call = d.getTime() - validation_last_call; var elapsed_since_last_call = d.getTime() - validation_last_call;
if (elapsed_since_last_call > 10000) { if (elapsed_since_last_call > 1000) {
loading2.show(); loading2.show();
validation_last_call = d.getTime(); validation_last_call = d.getTime();
var clicked = service_validation.find('.btn'); var rid = service_data.rid;
var rid = clicked.data('rid'); var mid = service_data.mid;
var mid = clicked.data('mid'); var sid = service_data.sid;
var sid = clicked.data('sid'); var stid = service_data.stid;
var stid = clicked.data('stid');
post_form( post_form(
'/members/service_presence/', '/members/service_presence/',
{'mid': mid, 'rid': rid, 'sid': sid, 'stid' : stid}, {'mid': mid, 'rid': rid, 'sid': sid, 'stid' : stid, 'cancel': false, 'type': e.data.type},
function(err, rData) { function(err, rData) {
if (!err) { if (!err) {
var res = rData.res; var res = rData.res;
...@@ -480,6 +501,28 @@ function record_service_presence() { ...@@ -480,6 +501,28 @@ function record_service_presence() {
} }
} }
function cancel_service_presence(mid, rid) {
var d = new Date();
var elapsed_since_last_call = d.getTime() - validation_last_call;
if (elapsed_since_last_call > 1000) {
loading2.show();
validation_last_call = d.getTime();
var sid = selected_service.id;
post_form(
'/members/service_presence/',
{'mid': mid, 'rid': rid, 'sid': sid, 'stid' : 0, 'cancel': true},
function(err) {
if (!err) {
get_service_entry_data();
}
loading2.hide();
}
);
}
}
function fill_rattrapage_2() { function fill_rattrapage_2() {
pages.rattrapage_2.find('span.member_name').text(current_displayed_member.name); pages.rattrapage_2.find('span.member_name').text(current_displayed_member.name);
var msg = "Bienvenue pour ton rattrapage !"; var msg = "Bienvenue pour ton rattrapage !";
...@@ -494,13 +537,11 @@ function fill_rattrapage_2() { ...@@ -494,13 +537,11 @@ function fill_rattrapage_2() {
msg = "Tu es en désincrit.e ... La situation doit être réglée avez le Bureau des Membres"; msg = "Tu es en désincrit.e ... La situation doit être réglée avez le Bureau des Membres";
} else { } else {
move_service_validation_to(pages.rattrapage_2); move_service_validation_to(pages.rattrapage_2);
service_data = {
rid : 0,
service_validation.find('.btn') sid : selected_service.id,
.data('rid', 0) stid : shift_ticket_id,
.data('sid', selected_service.id) mid : current_displayed_member.id};
.data('stid', shift_ticket_id)
.data('mid', current_displayed_member.id);
} }
pages.rattrapage_2.find('h2').text(msg); pages.rattrapage_2.find('h2').text(msg);
...@@ -641,7 +682,10 @@ $('.btn[data-next]').click(function() { ...@@ -641,7 +682,10 @@ $('.btn[data-next]').click(function() {
}); });
service_validation.on("click", ".btn", record_service_presence); service_validation.on("click", ".btn", {type:'normal'}, record_service_presence);
associated_service_validation.on("click", "#associated_btn", {type:'associate'}, record_service_presence);
associated_service_validation.on("click", "#partner_btn", {type:'partner'}, record_service_presence);
associated_service_validation.on("click", "#both_btn", {type:'both'}, record_service_presence);
shift_members.on("click", '.btn[data-rid]', function() { shift_members.on("click", '.btn[data-rid]', function() {
var clicked = $(this); var clicked = $(this);
...@@ -653,6 +697,14 @@ shift_members.on("click", '.btn[data-rid]', function() { ...@@ -653,6 +697,14 @@ shift_members.on("click", '.btn[data-rid]', function() {
}); });
shift_members.on("click", '.btn--inverse', function() {
var clicked = $(this);
var rid = clicked.data('rid');
var mid = clicked.data('mid');
cancel_service_presence(mid, rid);
});
pages.shopping_entry.on('css', function() { pages.shopping_entry.on('css', function() {
photo_advice.hide(); photo_advice.hide();
photo_studio.hide(); photo_studio.hide();
......
...@@ -32,6 +32,15 @@ function display_current_coop_form() { ...@@ -32,6 +32,15 @@ function display_current_coop_form() {
let street2_input = form.find('[name="street2"]'), let street2_input = form.find('[name="street2"]'),
phone_input = form.find('[name="phone"]'); phone_input = form.find('[name="phone"]');
if (current_coop.parent_name) {
$('#associated_member').show();
if (current_coop.parent_id)
$('#associated_member_name').text(current_coop.parent_name);
else $('#associated_member_name').text(current_coop.parent_name + " ATTENTION à faire manuellement");
} else {
$('#associated_member').hide();
}
chgt_shift_btn.hide(); chgt_shift_btn.hide();
chgt_shift_btn.off('click', open_shift_choice); chgt_shift_btn.off('click', open_shift_choice);
form.find('[name="firstname"]').val(current_coop.firstname); form.find('[name="firstname"]').val(current_coop.firstname);
......
...@@ -31,6 +31,7 @@ urlpatterns = [ ...@@ -31,6 +31,7 @@ urlpatterns = [
url(r'^latest_coop_id/$', views.latest_coop_id), url(r'^latest_coop_id/$', views.latest_coop_id),
url(r'^get/([0-9]+)$', views.get), url(r'^get/([0-9]+)$', views.get),
url(r'^exists/([a-zA-Z0-9_\-\.\+@]+)$', views.exists), url(r'^exists/([a-zA-Z0-9_\-\.\+@]+)$', views.exists),
url(r'^is_associated/([0-9]+)$', views.is_associated),
url(r'^get_couchdb_odoo_markers/(.+)$', views.get_couchdb_odoo_markers), url(r'^get_couchdb_odoo_markers/(.+)$', views.get_couchdb_odoo_markers),
url(r'^menu/$', views.menu), url(r'^menu/$', views.menu),
url(r'^verify_final_state$', views.verify_final_state), url(r'^verify_final_state$', views.verify_final_state),
......
...@@ -63,6 +63,10 @@ def exists(request, mail): ...@@ -63,6 +63,10 @@ def exists(request, mail):
answer = CagetteMember.exists(mail) answer = CagetteMember.exists(mail)
return JsonResponse({'answer': answer}) return JsonResponse({'answer': answer})
def is_associated(request, id_parent):
answer = CagetteMember.is_associated(id_parent)
return JsonResponse({'answer': answer})
def getmemberimage(request, id): def getmemberimage(request, id):
m = CagetteMember(id) m = CagetteMember(id)
call_res = m.get_image() call_res = m.get_image()
...@@ -97,7 +101,9 @@ def inscriptions(request, type=1): ...@@ -97,7 +101,9 @@ def inscriptions(request, type=1):
'POUCHDB_VERSION': getattr(settings, 'POUCHDB_VERSION', ''), 'POUCHDB_VERSION': getattr(settings, 'POUCHDB_VERSION', ''),
'max_chq_nb': getattr(settings, 'MAX_CHQ_NB', 12), 'max_chq_nb': getattr(settings, 'MAX_CHQ_NB', 12),
'show_ftop_button': getattr(settings, 'SHOW_FTOP_BUTTON', True), 'show_ftop_button': getattr(settings, 'SHOW_FTOP_BUTTON', True),
'db': settings.COUCHDB['dbs']['member']} 'db': settings.COUCHDB['dbs']['member'],
'ASSOCIATE_MEMBER_SHIFT' : getattr(settings, 'ASSOCIATE_MEMBER_SHIFT', '')
}
response = HttpResponse(template.render(context, request)) response = HttpResponse(template.render(context, request))
return response return response
...@@ -277,6 +283,9 @@ def record_service_presence(request): ...@@ -277,6 +283,9 @@ def record_service_presence(request):
mid = int(request.POST.get("mid", 0)) # member id mid = int(request.POST.get("mid", 0)) # member id
sid = int(request.POST.get("sid", 0)) # shift id sid = int(request.POST.get("sid", 0)) # shift id
stid = int(request.POST.get("stid", 0)) # shift_ticket_id stid = int(request.POST.get("stid", 0)) # shift_ticket_id
cancel = request.POST.get("cancel") == 'true'
typeAction = str(request.POST.get("type"))
app_env = getattr(settings, 'APP_ENV', "prod") app_env = getattr(settings, 'APP_ENV', "prod")
if (rid > -1 and mid > 0): if (rid > -1 and mid > 0):
overrided_date = "" overrided_date = ""
...@@ -286,14 +295,15 @@ def record_service_presence(request): ...@@ -286,14 +295,15 @@ def record_service_presence(request):
if o_date: if o_date:
overrided_date = re.sub(r'(%20)',' ', o_date.group(1)) overrided_date = re.sub(r'(%20)',' ', o_date.group(1))
if(not cancel):
# rid = 0 => C'est un rattrapage, sur le service # rid = 0 => C'est un rattrapage, sur le service
if sid > 0 and stid > 0: if sid > 0 and stid > 0:
# Add member to service and take presence into account # Add member to service and take presence into account
res['rattrapage'] = CagetteServices.record_rattrapage(mid, sid, stid) res['rattrapage'] = CagetteServices.record_rattrapage(mid, sid, stid, typeAction)
if res['rattrapage'] is True: if res['rattrapage'] is True:
res['update'] = 'ok' res['update'] = 'ok'
else: else:
if (CagetteServices.registration_done(rid, overrided_date) is True): if (CagetteServices.registration_done(rid, overrided_date, typeAction) is True):
res['update'] = 'ok' res['update'] = 'ok'
else: else:
res['update'] = 'ko' res['update'] = 'ko'
...@@ -308,6 +318,8 @@ def record_service_presence(request): ...@@ -308,6 +318,8 @@ def record_service_presence(request):
del m['shifts'] del m['shifts']
m['next_shift'] = next_shift m['next_shift'] = next_shift
res['member'] = m res['member'] = m
else: CagetteServices.reopen_registration(rid, overrided_date)
except Exception as e: except Exception as e:
res['error'] = str(e) res['error'] = str(e)
return JsonResponse({'res': res}) return JsonResponse({'res': res})
......
...@@ -54,6 +54,13 @@ ...@@ -54,6 +54,13 @@
margin: 3rem 0; margin: 3rem 0;
} }
#my_info .choose_makeups,
#my_info .unsuscribed_form_link,
#my_info .remove_future_registration {
font-size: 1.8rem;
word-break: normal;
}
#my_info #member_status_action, #my_info #member_status_action,
#my_info .member_shift_name_area, #my_info .member_shift_name_area,
#my_info .member_coop_number_area { #my_info .member_coop_number_area {
......
...@@ -64,14 +64,39 @@ ...@@ -64,14 +64,39 @@
} }
} }
.shift_line_container {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
}
.shift_line_extra_actions {
display: flex;
justify-content: flex-start;
flex-wrap: nowrap;
}
@media screen and (max-width:768px) {
.shift_line_container {
flex-direction: column;
}
.shift_line_extra_actions {
width: 100%;
}
.affect_associate_registered {
margin: 0.5rem 0;
}
}
.selectable_shift_line { .selectable_shift_line {
min-width: 325px; min-width: 325px;
display: flex; display: flex;
align-items: center; align-items: center;
margin-left: 15px;
margin: 0.75rem 0;
border-radius: 5px; border-radius: 5px;
margin-right: 15px;
} }
.selectable_shift_line .checkbox { .selectable_shift_line .checkbox {
...@@ -85,11 +110,31 @@ ...@@ -85,11 +110,31 @@
.affect_associate_registered { .affect_associate_registered {
display: flex; display: flex;
align-items: center; align-items: center;
margin-left: 15px;
margin: 0.75rem 0;
border-radius: 5px; border-radius: 5px;
} }
@media screen and (min-width:768px) {
.selectable_shift_line {
margin: 0 15px;
}
.affect_associate_registered {
margin-left: 15px;
}
}
.selectable_shift{
margin: 1rem 0;
}
.delete_registration_button {
justify-content: center;
align-items: center;
margin: 0.75rem 15px;
color: #d9534f;
cursor: pointer;
display: none;
}
/* -- Calendar screen, makeups message */ /* -- Calendar screen, makeups message */
...@@ -113,6 +158,21 @@ ...@@ -113,6 +158,21 @@
} }
} }
/* -- Calendar screen, can delete registrations message */
#can_delete_future_registrations_area {
display: none;
justify-content: center;
align-items: center;
margin: 0 1rem 1rem 1rem;
}
#can_delete_future_registrations_area button {
white-space: normal;
word-break: normal;
margin: 1rem;
}
/* -- Calendar screen, calendar */ /* -- Calendar screen, calendar */
#calendar { #calendar {
...@@ -157,6 +217,19 @@ td{ ...@@ -157,6 +217,19 @@ td{
color: white; color: white;
} }
.fc-event.shift_booked_makeup {
background-color: #f0ad4e;
cursor: auto;
border-color: #f0ad4e;
}
.fc-event.shift_booked_makeup td {
--fc-list-event-hover-bg-color:#f0ad4e;
}
.fc-list-event.shift_booked_makeup {
color: white;
}
#calendar .fc-list-table { #calendar .fc-list-table {
table-layout: auto; table-layout: auto;
} }
...@@ -205,3 +278,13 @@ td{ ...@@ -205,3 +278,13 @@ td{
max-width: 60%; max-width: 60%;
margin: 2rem auto 0.5rem auto; margin: 2rem auto 0.5rem auto;
} }
/* -- Assign shift modal */
.modal_affect_shift_buttons {
margin: 1rem 0;
}
.assign_shift_button {
margin: 0.25rem;
}
\ No newline at end of file
...@@ -166,6 +166,11 @@ body { ...@@ -166,6 +166,11 @@ body {
font-size: 1.5rem; font-size: 1.5rem;
} }
.remove_future_registration {
display: none;
white-space: normal;
}
.unsuscribed_form_link { .unsuscribed_form_link {
display: none; display: none;
text-decoration: none; text-decoration: none;
......
...@@ -68,8 +68,7 @@ function prepare_server_data(data) { ...@@ -68,8 +68,7 @@ function prepare_server_data(data) {
if (history_item.associate_registered == false || history_item.associate_registered == undefined) { if (history_item.associate_registered == false || history_item.associate_registered == undefined) {
history_item.associate_registered = ""; history_item.associate_registered = "";
} } else {
else {
if (partner_data.associated_partner_id != "False") { if (partner_data.associated_partner_id != "False") {
if (history_item.associate_registered==="partner") { if (history_item.associate_registered==="partner") {
history_item.associate_registered = partner_data.name; history_item.associate_registered = partner_data.name;
......
...@@ -222,6 +222,7 @@ function prepare_shift_line_template(date_begin) { ...@@ -222,6 +222,7 @@ function prepare_shift_line_template(date_begin) {
*/ */
function init_my_info_data() { function init_my_info_data() {
$(".choose_makeups").off(); $(".choose_makeups").off();
$(".remove_future_registration").off();
$(".unsuscribed_form_link").off(); $(".unsuscribed_form_link").off();
$(".member_shift_name").text(partner_data.regular_shift_name); $(".member_shift_name").text(partner_data.regular_shift_name);
...@@ -286,6 +287,13 @@ function init_my_info_data() { ...@@ -286,6 +287,13 @@ function init_my_info_data() {
} }
} }
if (partner_data.extra_shift_done > 0) {
$(".remove_future_registration").show();
$(".remove_future_registration").on('click', () => {
goto('echange-de-services');
});
}
$(".member_coop_number").text(partner_data.barcode_base); $(".member_coop_number").text(partner_data.barcode_base);
} }
...@@ -317,7 +325,7 @@ $(document).ready(function() { ...@@ -317,7 +325,7 @@ $(document).ready(function() {
// debouncing function from John Hann // debouncing function from John Hann
// http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/ // http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/
var debounce = function (func, threshold, execAsap) { var debounce = function (func, threshold, execAsap) {
var timeout; var timeout = null;
return function debounced () { return function debounced () {
var obj = this, args = arguments; var obj = this, args = arguments;
......
...@@ -12,5 +12,6 @@ urlpatterns = [ ...@@ -12,5 +12,6 @@ urlpatterns = [
url(r'^faqBDM$', views.faqBDM), url(r'^faqBDM$', views.faqBDM),
url(r'^no_content$', views.no_content), url(r'^no_content$', views.no_content),
url(r'^get_shifts_history$', views.get_shifts_history), url(r'^get_shifts_history$', views.get_shifts_history),
url(r'^offer_extra_shift$', views.offer_extra_shift),
url(r'^.*', views.index) # Urls unknown from the server will redirect to index url(r'^.*', views.index) # Urls unknown from the server will redirect to index
] ]
...@@ -102,6 +102,8 @@ def index(request, exception=None): ...@@ -102,6 +102,8 @@ def index(request, exception=None):
partnerData['makeups_to_do'] = partnerData['parent_makeups_to_do'] partnerData['makeups_to_do'] = partnerData['parent_makeups_to_do']
partnerData['date_delay_stop'] = partnerData['parent_date_delay_stop'] partnerData['date_delay_stop'] = partnerData['parent_date_delay_stop']
partnerData['can_have_delay'] = cs.member_can_have_delay(int(partnerData["parent_id"])) partnerData['can_have_delay'] = cs.member_can_have_delay(int(partnerData["parent_id"]))
partnerData['extra_shift_done'] = partnerData["parent_extra_shift_done"]
else: else:
partnerData["parent_name"] = False partnerData["parent_name"] = False
partnerData['can_have_delay'] = cs.member_can_have_delay(int(partner_id)) partnerData['can_have_delay'] = cs.member_can_have_delay(int(partner_id))
...@@ -236,3 +238,12 @@ def get_shifts_history(request): ...@@ -236,3 +238,12 @@ def get_shifts_history(request):
res["data"] = m.get_shifts_history(partner_id, limit, offset, date_from) res["data"] = m.get_shifts_history(partner_id, limit, offset, date_from)
return JsonResponse(res) return JsonResponse(res)
def offer_extra_shift(request):
res = {}
partner_id = int(request.POST['partner_id'])
m = CagetteMember(partner_id)
res = m.update_extra_shift_done(0)
return JsonResponse(res)
...@@ -9,7 +9,8 @@ var suppliers_list = [], ...@@ -9,7 +9,8 @@ var suppliers_list = [],
new_product_supplier_association = { new_product_supplier_association = {
package_qty: null, package_qty: null,
price: null price: null
}; },
qties_values = {};
var dbc = null, var dbc = null,
sync = null, sync = null,
...@@ -30,6 +31,10 @@ var dbc = null, ...@@ -30,6 +31,10 @@ var dbc = null,
var clicked_order_pill = null; var clicked_order_pill = null;
let userAgent = navigator.userAgent;
var timerId; var timerId;
/* - UTILS */ /* - UTILS */
...@@ -124,12 +129,74 @@ function debounceFunction(func, delay = 1000) { ...@@ -124,12 +129,74 @@ function debounceFunction(func, delay = 1000) {
timerId = setTimeout(func, delay); timerId = setTimeout(func, delay);
} }
/* - PRODUCTS */ /* - PRODUCTS */
var process_new_product_qty = function(input) {
// Remove line coloring on input blur
const row = $(input).closest('tr');
row.removeClass('focused_line');
let val = ($(input).val() == '') ? 0 : $(input).val();
const id_split = $(input).attr('id')
.split('_');
const prod_id = id_split[1];
const supplier_id = id_split[3];
if (val == -1) {
let modal_end_supplier_product_association = $('#templates #modal_end_supplier_product_association');
const product = products.find(p => p.id == prod_id);
modal_end_supplier_product_association.find(".product_name").text(product.name);
const supplier = selected_suppliers.find(s => s.id == supplier_id);
modal_end_supplier_product_association.find(".supplier_name").text(supplier.display_name);
openModal(
modal_end_supplier_product_association.html(),
() => {
if (is_time_to('validate_end_supplier_product_association')) {
end_supplier_product_association(product, supplier);
}
},
'Valider',
false,
true,
() => {
// Reset value in input on cancel
const psi = product.suppliersinfo.find(psi_item => psi_item.supplier_id == supplier_id);
$(input).val(psi.qty);
}
);
} else {
val = parseFloat(val);
// If value is a number
if (!isNaN(val)) {
// Save value
save_product_supplier_qty(prod_id, supplier_id, val);
// Update row
const product = products.find(p => p.id == prod_id);
const new_row_data = prepare_datatable_data([product.id])[0];
products_table.row($(input).closest('tr')).data(new_row_data)
.draw();
debounceFunction(update_cdb_order);
display_total_values();
} else {
$(input).val('');
}
}
};
/** /**
* Add a product. * Add a product.
* *
* @returns -1 if validation failed, 0 otherwise * @returns -1 if validation failed, 0 otherwise
*/ */
function add_product() { function add_product() {
const user_input = $("#product_input").val(); const user_input = $("#product_input").val();
...@@ -191,7 +258,13 @@ function add_product() { ...@@ -191,7 +258,13 @@ function add_product() {
return 0; return 0;
} }
function compute_purchase_qty_for_coverage(product, coeff, stock, incoming_qty, daily_conso, days) { function compute_purchase_qty_for_coverage(product, coeff, stock, incoming_qty, daily_conso, days) {
let purchase_qty_for_coverage = null; let purchase_qty_for_coverage = 0,
purchase_package_qty_for_coverage = 0;
if (stock == 0 && daily_conso == 0) {
purchase_package_qty_for_coverage = 1;
} else {
purchase_qty_for_coverage = days * daily_conso - stock - incoming_qty + product.minimal_stock; purchase_qty_for_coverage = days * daily_conso - stock - incoming_qty + product.minimal_stock;
purchase_qty_for_coverage = (purchase_qty_for_coverage < 0) ? 0 : purchase_qty_for_coverage; purchase_qty_for_coverage = (purchase_qty_for_coverage < 0) ? 0 : purchase_qty_for_coverage;
...@@ -201,6 +274,7 @@ function compute_purchase_qty_for_coverage(product, coeff, stock, incoming_qty, ...@@ -201,6 +274,7 @@ function compute_purchase_qty_for_coverage(product, coeff, stock, incoming_qty,
if (coeff != 1) { if (coeff != 1) {
purchase_package_qty_for_coverage *= coeff; purchase_package_qty_for_coverage *= coeff;
} }
}
// return Round up to unit for all products // return Round up to unit for all products
return Math.ceil(purchase_package_qty_for_coverage); return Math.ceil(purchase_package_qty_for_coverage);
} }
...@@ -217,6 +291,7 @@ function compute_and_affect_product_supplier_quantities(coeff, days) { ...@@ -217,6 +291,7 @@ function compute_and_affect_product_supplier_quantities(coeff, days) {
const stock = product.qty_available; const stock = product.qty_available;
const incoming_qty = product.incoming_qty; const incoming_qty = product.incoming_qty;
const daily_conso = product.daily_conso; const daily_conso = product.daily_conso;
purchase_package_qty_for_coverage = compute_purchase_qty_for_coverage(product, coeff, stock, incoming_qty, daily_conso, days); purchase_package_qty_for_coverage = compute_purchase_qty_for_coverage(product, coeff, stock, incoming_qty, daily_conso, days);
// Set qty to purchase for first supplier only // Set qty to purchase for first supplier only
products[key].suppliersinfo[0].qty = purchase_package_qty_for_coverage; products[key].suppliersinfo[0].qty = purchase_package_qty_for_coverage;
...@@ -234,6 +309,7 @@ function compute_products_coverage_qties() { ...@@ -234,6 +309,7 @@ function compute_products_coverage_qties() {
return new Promise((resolve) => { return new Promise((resolve) => {
const pc_adjust = $('#percent_adjust_input').val(); const pc_adjust = $('#percent_adjust_input').val();
let coeff = 1; let coeff = 1;
if (!isNaN(parseFloat(pc_adjust))) { if (!isNaN(parseFloat(pc_adjust))) {
coeff = (1 + parseFloat(pc_adjust) /100); coeff = (1 + parseFloat(pc_adjust) /100);
} }
...@@ -249,7 +325,7 @@ function compute_products_coverage_qties() { ...@@ -249,7 +325,7 @@ function compute_products_coverage_qties() {
step = 1; step = 1;
//Let's compute the nearst amount, by changing days quantity //Let's compute the nearst amount, by changing days quantity
while(go_on == true && iter < max_iter) { while (go_on == true && iter < max_iter) {
order_total_value = 0; order_total_value = 0;
compute_and_affect_product_supplier_quantities(coeff, days); compute_and_affect_product_supplier_quantities(coeff, days);
_compute_total_values_by_supplier(); _compute_total_values_by_supplier();
...@@ -1674,68 +1750,11 @@ function display_products(params) { ...@@ -1674,68 +1750,11 @@ function display_products(params) {
row.addClass('focused_line'); row.addClass('focused_line');
}); });
// Manage data on inputs blur
$('#products_table').on('blur', 'tbody td .product_qty_input', function () {
// Remove line coloring on input blur
const row = $(this).closest('tr');
row.removeClass('focused_line');
let val = ($(this).val() == '') ? 0 : $(this).val();
const id_split = $(this).attr('id')
.split('_');
const prod_id = id_split[1];
const supplier_id = id_split[3];
if (val == -1) {
let modal_end_supplier_product_association = $('#templates #modal_end_supplier_product_association');
const product = products.find(p => p.id == prod_id);
modal_end_supplier_product_association.find(".product_name").text(product.name);
const supplier = selected_suppliers.find(s => s.id == supplier_id);
modal_end_supplier_product_association.find(".supplier_name").text(supplier.display_name);
openModal(
modal_end_supplier_product_association.html(),
() => {
if (is_time_to('validate_end_supplier_product_association')) {
end_supplier_product_association(product, supplier);
}
},
'Valider',
false,
true,
() => {
// Reset value in input on cancel
const psi = product.suppliersinfo.find(psi_item => psi_item.supplier_id == supplier_id);
$(this).val(psi.qty);
}
);
} else {
val = parseFloat(val);
// If value is a number // Manage data on inputs blur
if (!isNaN(val)) { $('#products_table')
// Save value .on('blur', 'tbody td .product_qty_input', function () {
save_product_supplier_qty(prod_id, supplier_id, val); process_new_product_qty(this);
// Update row
const product = products.find(p => p.id == prod_id);
const new_row_data = prepare_datatable_data([product.id])[0];
products_table.row($(this).closest('tr')).data(new_row_data)
.draw();
debounceFunction(update_cdb_order);
display_total_values();
} else {
$(this).val('');
}
}
}) })
.on('keypress', 'tbody td .product_qty_input', function(e) { .on('keypress', 'tbody td .product_qty_input', function(e) {
// Validate on Enter pressed // Validate on Enter pressed
...@@ -2254,12 +2273,12 @@ $(document).ready(function() { ...@@ -2254,12 +2273,12 @@ $(document).ready(function() {
.then(() => { .then(() => {
debounceFunction(update_cdb_order); debounceFunction(update_cdb_order);
update_main_screen(); update_main_screen();
}) });
} else { } else {
$("#coverage_days_input").val(order_doc.coverage_days || ''); $("#coverage_days_input").val(order_doc.coverage_days || '');
$('#targeted_amount_input').val(order_doc.targeted_amount || ''); $('#targeted_amount_input').val(order_doc.targeted_amount || '');
alert("Ni le nombre de jours de couverture, ni le montant à atteindre sont correctement renseignés") alert("Ni le nombre de jours de couverture, ni le montant à atteindre sont correctement renseignés");
} }
} }
...@@ -2319,7 +2338,7 @@ $(document).ready(function() { ...@@ -2319,7 +2338,7 @@ $(document).ready(function() {
update_main_screen(); update_main_screen();
debounceFunction(update_cdb_order); debounceFunction(update_cdb_order);
closeModal(); closeModal();
}) });
}); });
} }
...@@ -2590,6 +2609,30 @@ $(document).ready(function() { ...@@ -2590,6 +2609,30 @@ $(document).ready(function() {
panel.style.display = "block"; panel.style.display = "block";
} }
}); });
if (/Firefox\//.exec(userAgent)) {
// needed to prevent bug using number input arrow to change quantity (https://bugzilla.mozilla.org/show_bug.cgi?id=1012818)
// Have to capture mousedown and mouseup events, instead of using only click event
// Indeed, capturing click only remove the ability to click to have focus on the input to type a number.
$(document).on("mousedown", '[type="number"]', function() {
const clicked = this;
qties_values[$(clicked).attr('id')] = $(clicked).val();
});
$(document).on("mouseup", '[type="number"]', function() {
const clicked = this;
try {
if ($(clicked).val() != qties_values[$(clicked).attr('id')]) {
process_new_product_qty(clicked);
}
} catch (err) {
console.log(err);
}
});
}
} else { } else {
$('#not_connected_content').show(); $('#not_connected_content').show();
} }
......
...@@ -129,6 +129,10 @@ ...@@ -129,6 +129,10 @@
La Cagette use False to implement custom rules La Cagette use False to implement custom rules
- ASSOCIATE_MEMBER_SHIFT = ''
Id number of the associate shift template
### Scales and labels files generation ### Scales and labels files generation
- DAV_PATH = '/data/dav/cagette' - DAV_PATH = '/data/dav/cagette'
......
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
.red {color:#FF0000;} .red {color:#FF0000;}
.b_red, .b_less_than_25pc {background:#ff3333 !important;} .b_red, .b_less_than_25pc {background:#ff3333 !important;}
.loading {background-image: url("/static/img/ajax-loader.gif"); background-repeat:no-repeat;} .loading {background-image: url("/static/img/ajax-loader.gif"); background-repeat:no-repeat;}
.loading2 {display: none;} .loading2 {display: none; position:absolute; top:-20px;}
.loading2-container {position:relative;}
body {background: #fff; margin:5px;} body {background: #fff; margin:5px;}
a, a:active, a:focus, a, a:active, a:focus,
......
...@@ -35,14 +35,15 @@ function get_shift_name(s_data) { ...@@ -35,14 +35,15 @@ function get_shift_name(s_data) {
if (s_data && s_data.week) { if (s_data && s_data.week) {
shift_name = weeks_name[s_data.week]; shift_name = weeks_name[s_data.week];
if (s_data.type == 2 && typeof manage_ftop != "undefined" && manage_ftop == true) { if (s_data.type == 2 && typeof manage_ftop != "undefined" && manage_ftop == true && s_data.id != ASSOCIATE_MEMBER_SHIFT) {
shift_name = 'Volant'; shift_name = 'Volant';
} else if(s_data.id == ASSOCIATE_MEMBER_SHIFT) {
shift_name = 'Binôme';
} else { } else {
shift_name += s_data.day + ' - ' + s_data.begin; shift_name += s_data.day + ' - ' + s_data.begin;
shift_name += ' - ' + s_data.place; shift_name += ' - ' + s_data.place;
} }
} }
return shift_name; return shift_name;
} }
......
...@@ -38,19 +38,20 @@ class CagetteShift(models.Model): ...@@ -38,19 +38,20 @@ class CagetteShift(models.Model):
'cooperative_state', 'final_standard_point', 'create_date', 'cooperative_state', 'final_standard_point', 'create_date',
'final_ftop_point', 'shift_type', 'leave_ids', 'makeups_to_do', 'barcode_base', 'final_ftop_point', 'shift_type', 'leave_ids', 'makeups_to_do', 'barcode_base',
'street', 'street2', 'zip', 'city', 'mobile', 'phone', 'email', 'street', 'street2', 'zip', 'city', 'mobile', 'phone', 'email',
'is_associated_people', 'parent_id'] 'is_associated_people', 'parent_id', 'extra_shift_done']
partnerData = self.o_api.search_read('res.partner', cond, fields, 1) partnerData = self.o_api.search_read('res.partner', cond, fields, 1)
if partnerData: if partnerData:
partnerData = partnerData[0] partnerData = partnerData[0]
if partnerData['is_associated_people']: if partnerData['is_associated_people']:
cond = [['id', '=', partnerData['parent_id'][0]]] cond = [['id', '=', partnerData['parent_id'][0]]]
fields = ['create_date', 'makeups_to_do', 'date_delay_stop'] fields = ['create_date', 'makeups_to_do', 'date_delay_stop', 'extra_shift_done']
parentData = self.o_api.search_read('res.partner', cond, fields, 1) parentData = self.o_api.search_read('res.partner', cond, fields, 1)
if parentData: if parentData:
partnerData['parent_create_date'] = parentData[0]['create_date'] partnerData['parent_create_date'] = parentData[0]['create_date']
partnerData['parent_makeups_to_do'] = parentData[0]['makeups_to_do'] partnerData['parent_makeups_to_do'] = parentData[0]['makeups_to_do']
partnerData['parent_date_delay_stop'] = parentData[0]['date_delay_stop'] partnerData['parent_date_delay_stop'] = parentData[0]['date_delay_stop']
partnerData['parent_extra_shift_done'] = parentData[0]['extra_shift_done']
if partnerData['shift_type'] == 'standard': if partnerData['shift_type'] == 'standard':
partnerData['in_ftop_team'] = False partnerData['in_ftop_team'] = False
...@@ -93,7 +94,7 @@ class CagetteShift(models.Model): ...@@ -93,7 +94,7 @@ class CagetteShift(models.Model):
def get_shift_partner(self, id): def get_shift_partner(self, id):
"""Récupère les shift du membre""" """Récupère les shift du membre"""
fields = ['date_begin', 'date_end','final_standard_point', fields = ['date_begin', 'date_end','final_standard_point',
'shift_id', 'shift_type','partner_id', "id", "associate_registered"] # res.partner 'shift_id', 'shift_type','partner_id', "id", "associate_registered", "is_makeup"] # res.partner
cond = [['partner_id.id', '=', id],['state', '=', 'open'], cond = [['partner_id.id', '=', id],['state', '=', 'open'],
['date_begin', '>', datetime.datetime.now().isoformat()]] ['date_begin', '>', datetime.datetime.now().isoformat()]]
shiftData = self.o_api.search_read('shift.registration', cond, fields, order ="date_begin ASC") shiftData = self.o_api.search_read('shift.registration', cond, fields, order ="date_begin ASC")
...@@ -369,3 +370,7 @@ class CagetteShift(models.Model): ...@@ -369,3 +370,7 @@ class CagetteShift(models.Model):
def member_can_have_delay(self, partner_id): def member_can_have_delay(self, partner_id):
""" Can a member have a delay? """ """ Can a member have a delay? """
return self.o_api.execute('res.partner', 'can_have_extension', [partner_id]) return self.o_api.execute('res.partner', 'can_have_extension', [partner_id])
def update_counter_event(self, fields):
""" Add/remove points """
return self.o_api.create('shift.counter.event', fields)
\ No newline at end of file
...@@ -15,6 +15,7 @@ urlpatterns = [ ...@@ -15,6 +15,7 @@ urlpatterns = [
url(r'^change_shift', views.change_shift), url(r'^change_shift', views.change_shift),
url(r'^affect_shift', views.affect_shift), url(r'^affect_shift', views.affect_shift),
url(r'^add_shift', views.add_shift), url(r'^add_shift', views.add_shift),
url(r'^cancel_shift', views.cancel_shift),
url(r'^request_delay', views.request_delay), url(r'^request_delay', views.request_delay),
url(r'^reset_members_positive_points', views.reset_members_positive_points) url(r'^reset_members_positive_points', views.reset_members_positive_points)
] ]
...@@ -101,8 +101,11 @@ def get_list_shift_calendar(request, partner_id): ...@@ -101,8 +101,11 @@ def get_list_shift_calendar(request, partner_id):
use_new_members_space = getattr(settings, 'USE_NEW_MEMBERS_SPACE', False) use_new_members_space = getattr(settings, 'USE_NEW_MEMBERS_SPACE', False)
listRegisterPartner = [] listRegisterPartner = []
listMakeUpShift = []
for v in registerPartner: for v in registerPartner:
listRegisterPartner.append(v['id']) listRegisterPartner.append(v['id'])
if v['is_makeup']:
listMakeUpShift.append(v['id'])
start = request.GET.get('start') start = request.GET.get('start')
end = request.GET.get('end') end = request.GET.get('end')
...@@ -136,6 +139,9 @@ def get_list_shift_calendar(request, partner_id): ...@@ -136,6 +139,9 @@ def get_list_shift_calendar(request, partner_id):
if len(l) > 0: if len(l) > 0:
if use_new_members_space is True: if use_new_members_space is True:
if set(value['registration_ids']) & set(listRegisterPartner) & set(listMakeUpShift):
event["classNames"] = ["shift_booked_makeup"]
else :
event["classNames"] = ["shift_booked"] event["classNames"] = ["shift_booked"]
else: else:
event["className"] = "shift_booked" event["className"] = "shift_booked"
...@@ -310,6 +316,37 @@ def add_shift(request): ...@@ -310,6 +316,37 @@ def add_shift(request):
else: else:
return HttpResponseForbidden() return HttpResponseForbidden()
def cancel_shift(request):
""" Annule une présence à un shift """
if 'verif_token' in request.POST:
partner_id = int(request.POST.get('idPartner'))
if Verification.verif_token(request.POST.get('verif_token'), partner_id) is True:
cs = CagetteShift()
listRegister = [int(request.POST['idRegister'])]
try:
response = cs.cancel_shift(listRegister)
# decrement extra_shift_done if param exists
if 'extra_shift_done' in request.POST:
target = int(request.POST["extra_shift_done"]) - 1
# extra security
if target < 0:
target = 0
cm = CagetteMember(partner_id)
cm.update_extra_shift_done(target)
return JsonResponse({"res" : 'response'})
except Exception as e:
return JsonResponse({"error" : str(e)}, status=500)
else:
return HttpResponseForbidden()
else:
return HttpResponseForbidden()
def request_delay(request): def request_delay(request):
if 'verif_token' in request.POST: if 'verif_token' in request.POST:
if Verification.verif_token(request.POST.get('verif_token'), int(request.POST.get('idPartner'))) is True: if Verification.verif_token(request.POST.get('verif_token'), int(request.POST.get('idPartner'))) is True:
......
...@@ -178,10 +178,24 @@ ...@@ -178,10 +178,24 @@
<section id="service_validation" class="col-6 grid-6 has-gutter"> <section id="service_validation" class="col-6 grid-6 has-gutter">
<div class="col-2"></div> <div class="col-2"></div>
<a class="col-2 btn present">{{CONFIRME_PRESENT_BTN|safe}}</a> <a class="col-2 btn present">{{CONFIRME_PRESENT_BTN|safe}}</a>
<div class="col-2 loading2-container">
<span class="loading2"><img width="75" src="/static/img/Pedro_luis_romani_ruiz.gif" alt="Chargement en cours...." /></span> <span class="loading2"><img width="75" src="/static/img/Pedro_luis_romani_ruiz.gif" alt="Chargement en cours...." /></span>
<div class="col-2"></div> </div>
</section>
<div id="associated_service_validation">
<p class="col-6 txtcenter">Qui est présent à ce service ?</p>
<section class="col-6 grid-5 has-gutter">
<div class="col-1"></div>
<a id="associated_btn" class=" btn present">Membre</a>
<a id="partner_btn" class=" btn present">Associé</a>
<a id="both_btn" class=" btn present">Les deux</a>
<div class="col-1 loading2-container">
<span class="loading2"><img width="75" src="/static/img/Pedro_luis_romani_ruiz.gif" alt="Chargement en cours...." /></span>
</div>
</section> </section>
</div> </div>
</div>
<div class="col-2"></div> <div class="col-2"></div>
<a class="btn col-2 return" data-next="service_entry">Retour</a> <a class="btn col-2 return" data-next="service_entry">Retour</a>
<div class="col-2"></div> <div class="col-2"></div>
......
...@@ -65,14 +65,55 @@ ...@@ -65,14 +65,55 @@
<input type="number" min="1" placeholder="Nb de chèques" name="ch_qty" id="ch_qty" style="display:none;"/> <input type="number" min="1" placeholder="Nb de chèques" name="ch_qty" id="ch_qty" style="display:none;"/>
</p> </p>
{% if input_barcode %} {% if input_barcode %}
<p> <p>
<input type="text" name="m_barcode" id="m_barcode" maxlength="13" size="13" placeholder="Code barre" autocomplete="off" required/> <input type="text" name="m_barcode" id="m_barcode" maxlength="13" size="13" placeholder="Code barre" autocomplete="off" required/>
</p> </p>
{% endif %} {% endif %}
{% if ASSOCIATE_MEMBER_SHIFT %}
<p id="add_binome" >+ Binomes (facultatif)</p>
<div id="associate_area" style="display:none;">
<div class="choice_button_area d-flex" >
<div id="existing_member_choice" class="member_choice">
A mettre en binome avec un.e membre existant.e
</div>
<div id="new_member_choice" class="member_choice">
A mettre en binome avec un.e nouveau membre
</div>
</div>
<div id="existing_member_choice_action" style="display:none;">
<input type="text" id="search_member_input" value="" placeholder="Nom ou numéro du coop..." >
<div class="btn--primary" id="search_member_button">Recherche</div>
<div class="search_member_results_area" style="display:none;">
<div class="search_results_text">
<p><i>Choisissez parmi les membres trouvés :</i></p>
</div>
<div class="search_member_results"></div>
</div>
<div class="chosen_associate_area" style="display:none;">
<div >
<p><i>Binôme choisit : </i></p>
</div>
<div class="chosen_associate"></div>
</div>
</div>
<div id="new_member_choice_action" style="display:none;">
<div >
<div>
<input type="text" id="new_member_input" value="" placeholder="Nom du membre" >
</div>
</div>
</div>
</div>
{% endif %}
<div>
<button class="btn--primary">Valider</button> <button class="btn--primary">Valider</button>
</form>
</div> </div>
</form>
<div id="mail_generation"> <div id="mail_generation">
(*) L'adresse mail étant obligatoire, si le nouveau membre n'en a pas, veuillez en créer une en cliquant sur le bouton suivant : <a class="btn--info" id="generate_email">+</a> (*) L'adresse mail étant obligatoire, si le nouveau membre n'en a pas, veuillez en créer une en cliquant sur le bouton suivant : <a class="btn--info" id="generate_email">+</a>
</div> </div>
...@@ -119,6 +160,7 @@ ...@@ -119,6 +160,7 @@
var couchdb_dbname = '{{db}}'; var couchdb_dbname = '{{db}}';
var couchdb_server = '{{couchdb_server}}' + couchdb_dbname; var couchdb_server = '{{couchdb_server}}' + couchdb_dbname;
var dbc = new PouchDB(couchdb_dbname); var dbc = new PouchDB(couchdb_dbname);
var ASSOCIATE_MEMBER_SHIFT = '{{ASSOCIATE_MEMBER_SHIFT}}';
var sync = PouchDB.sync(couchdb_dbname, couchdb_server, { var sync = PouchDB.sync(couchdb_dbname, couchdb_server, {
live: true, live: true,
retry: true, retry: true,
......
...@@ -60,6 +60,9 @@ ...@@ -60,6 +60,9 @@
<input type="text" name="m_barcode" id="m_barcode" disabled/> <input type="text" name="m_barcode" id="m_barcode" disabled/>
</p> </p>
{% endif %} {% endif %}
<div id="associated_member">
En binôme avec : <span id ="associated_member_name"></span>
</div>
</div> </div>
<p class="buttons"> <p class="buttons">
<button class="btn--success" name="valider">Tout est bon</button> <button class="btn--success" name="valider">Tout est bon</button>
......
...@@ -21,6 +21,9 @@ ...@@ -21,6 +21,9 @@
<button type="button" class="btn--danger choose_makeups"> <button type="button" class="btn--danger choose_makeups">
Je sélectionne mes rattrapages Je sélectionne mes rattrapages
</button> </button>
<button type="button" class="btn--success remove_future_registration">
J'ai validé un service à deux, je peux supprimer une présence
</button>
</div> </div>
<div class="member_shift_name_area"> <div class="member_shift_name_area">
<span>Mon créneau : </span> <span>Mon créneau : </span>
......
...@@ -36,29 +36,36 @@ ...@@ -36,29 +36,36 @@
</div> </div>
<div id="selectable_shift_line_template"> <div id="selectable_shift_line_template">
<div class="d-flex"> <div class="d-flex shift_line_container selectable_shift">
<div class="selectable_shift_line btn--primary"> <div class="selectable_shift_line btn--primary">
<input type="checkbox" class="checkbox"> <input type="checkbox" class="checkbox">
<div class="selectable_shift_line_text"> <div class="selectable_shift_line_text">
<span class="shift_line_date"></span> - <span class="shift_line_time"></span> <span class="shift_line_date"></span> - <span class="shift_line_time"></span>
</div> </div>
</div> </div>
<div class="affect_associate_registered button--warning"> <div class="shift_line_extra_actions">
<div class="affect_associate_registered">
</div> </div>
</div> </div>
</div> </div>
</div>
<div id="delete_registration_button_template">
<div class="delete_registration_button"><i class="fas fa-lg fa-trash"></i></div>
</div>
<div id="modal_affect_shift"> <div id="modal_affect_shift">
<div>Qui sera présent.e ?</div> <div>Qui sera présent.e ?</div>
<div id="shift_partner" class="btn--primary"> <div class="modal_affect_shift_buttons">
<div id="shift_partner" class="btn--primary assign_shift_button">
</div> </div>
<div id="shift_associate" class=" btn--primary"> <div id="shift_associate" class=" btn--primary assign_shift_button">
</div> </div>
<div id="shift_both" class=" btn--primary"> <div id="shift_both" class=" btn--primary assign_shift_button">
Les deux Les deux
</div> </div>
</div>
</div> </div>
<div id="modal_shift_exchange_template"> <div id="modal_shift_exchange_template">
...@@ -74,10 +81,12 @@ ...@@ -74,10 +81,12 @@
<div id="calendar_explaination_template"> <div id="calendar_explaination_template">
<h4>Légende du calendrier</h4> <h4>Légende du calendrier</h4>
<a class="example-event fc-daygrid-event fc-daygrid-block-event fc-h-event fc-event fc-event-start fc-event-end fc-event-future shift_booked"><div class="fc-event-main"><div class="fc-event-main-frame"><div class="fc-event-time">06:00</div><div class="fc-event-title-container"><div class="fc-event-title fc-sticky">&nbsp;- 9/12</div></div></div></div></a>
<p>Un service colorié en noir : je suis déjà inscrit.e à ce service.</p>
<a class="example-event fc-daygrid-event fc-daygrid-block-event fc-h-event fc-event fc-event-start fc-event-end fc-event-future shift_less_alf"><div class="fc-event-main"><div class="fc-event-main-frame"><div class="fc-event-time">10:45</div><div class="fc-event-title-container"><div class="fc-event-title fc-sticky">&nbsp;- 3/12</div></div></div></div></a> <a class="example-event fc-daygrid-event fc-daygrid-block-event fc-h-event fc-event fc-event-start fc-event-end fc-event-future shift_less_alf"><div class="fc-event-main"><div class="fc-event-main-frame"><div class="fc-event-time">10:45</div><div class="fc-event-title-container"><div class="fc-event-title fc-sticky">&nbsp;- 3/12</div></div></div></div></a>
<p>Un service colorié en bleu : je peux m'inscrire à ce service.</p> <p>Un service colorié en bleu : je peux m'inscrire à ce service.</p>
<a class="example-event fc-daygrid-event fc-daygrid-block-event fc-h-event fc-event fc-event-start fc-event-end fc-event-future shift_booked"><div class="fc-event-main"><div class="fc-event-main-frame"><div class="fc-event-time">06:00</div><div class="fc-event-title-container"><div class="fc-event-title fc-sticky">&nbsp;- 9/12</div></div></div></div></a>
<p>Un service colorié en noir : je suis déjà inscrit.e à ce service.</p>
<a class="example-event fc-daygrid-event fc-daygrid-block-event fc-h-event fc-event fc-event-start fc-event-end fc-event-future shift_booked_makeup"><div class="fc-event-main"><div class="fc-event-main-frame"><div class="fc-event-time">13:30</div><div class="fc-event-title-container"><div class="fc-event-title fc-sticky">&nbsp;- 7/12</div></div></div></div></a>
<p>Un service colorié en orange : je suis inscrit.e à un rattrapage sur ce service.</p>
<p>3/12 <i class="arrow_explanation_numbers fas fa-arrow-right"></i> il y a déjà 3 places réservées à ce service sur 12 disponibles. <p>3/12 <i class="arrow_explanation_numbers fas fa-arrow-right"></i> il y a déjà 3 places réservées à ce service sur 12 disponibles.
<b>Plus le chiffre de gauche est petit, plus on a besoin de coopérateurs.rices à ce service !</b></p> <b>Plus le chiffre de gauche est petit, plus on a besoin de coopérateurs.rices à ce service !</b></p>
</div> </div>
...@@ -140,7 +149,8 @@ ...@@ -140,7 +149,8 @@
"associated_partner_name" : "{{partnerData.associated_partner_name}}", "associated_partner_name" : "{{partnerData.associated_partner_name}}",
"verif_token" : "{{partnerData.verif_token}}", "verif_token" : "{{partnerData.verif_token}}",
"leave_stop_date": "{{partnerData.leave_stop_date}}", "leave_stop_date": "{{partnerData.leave_stop_date}}",
"comite": "{{partnerData.comite}}" "comite": "{{partnerData.comite}}",
"extra_shift_done": parseInt("{{partnerData.extra_shift_done}}", 10)
}; };
var block_actions_for_attached_people = '{{block_actions_for_attached_people}}'; var block_actions_for_attached_people = '{{block_actions_for_attached_people}}';
</script> </script>
......
...@@ -25,6 +25,9 @@ ...@@ -25,6 +25,9 @@
<button type="button" class="btn--danger choose_makeups"> <button type="button" class="btn--danger choose_makeups">
Je sélectionne mes rattrapages Je sélectionne mes rattrapages
</button> </button>
<button type="button" class="btn--success remove_future_registration">
J'ai validé un service à deux, je peux supprimer une présence
</button>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -35,6 +35,14 @@ ...@@ -35,6 +35,14 @@
<span class="select_makeups_message_block">Je dois les sélectionner dans le calendrier. </span> <span class="select_makeups_message_block">Je dois les sélectionner dans le calendrier. </span>
<span class="select_makeups_message_block">Je ne peux pas échanger de service tant que je n'ai pas choisi mes rattrapages. </span> <span class="select_makeups_message_block">Je ne peux pas échanger de service tant que je n'ai pas choisi mes rattrapages. </span>
</div> </div>
<div id="can_delete_future_registrations_area">
<button class="btn--success can_delete_future_registrations_button" id="delete_future_registration">
J'ai validé <span class="extra_shift_done"></span> service(s) à deux, je supprime un service futur
</button>
<button class="btn--success can_delete_future_registrations_button" id="offer_extra_shift">
Je souhaite donner <span class="extra_shift_done"></span> service(s) d'avance à la communauté
</button>
</div>
<div id="calendar_top_info"> <div id="calendar_top_info">
<div id="partner_shifts_list"> <div id="partner_shifts_list">
<h4>Liste de mes services :</h4> <h4>Liste de mes services :</h4>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment